diff --git a/CHANGELOG.md b/CHANGELOG.md index b8baab261..752d9dde4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ accidentally triggering the load of a previous DB version.** * #4393 Support intervals with day component when constifying now() * #4397 Support intervals with month component when constifying now() * #4641 Allow bucketing by month in time_bucket +* #4642 Add timezone support to time_bucket **Bugfixes** * #4416 Handle TRUNCATE TABLE on chunks diff --git a/sql/time_bucket.sql b/sql/time_bucket.sql index a3ba59bf6..c8c847ee6 100644 --- a/sql/time_bucket.sql +++ b/sql/time_bucket.sql @@ -23,6 +23,10 @@ CREATE OR REPLACE FUNCTION @extschema@.time_bucket(bucket_width INTERVAL, ts TIM CREATE OR REPLACE FUNCTION @extschema@.time_bucket(bucket_width INTERVAL, ts DATE, origin DATE) RETURNS DATE AS '@MODULE_PATHNAME@', 'ts_date_bucket' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; +-- bucketing with timezone +CREATE OR REPLACE FUNCTION @extschema@.time_bucket(bucket_width INTERVAL, ts TIMESTAMPTZ, timezone TEXT, origin TIMESTAMPTZ DEFAULT NULL, "offset" INTERVAL DEFAULT NULL) RETURNS TIMESTAMPTZ + AS '@MODULE_PATHNAME@', 'ts_timestamptz_timezone_bucket' LANGUAGE C IMMUTABLE PARALLEL SAFE; + -- bucketing of int CREATE OR REPLACE FUNCTION @extschema@.time_bucket(bucket_width SMALLINT, ts SMALLINT) RETURNS SMALLINT AS '@MODULE_PATHNAME@', 'ts_int16_bucket' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; diff --git a/sql/updates/reverse-dev.sql b/sql/updates/reverse-dev.sql index f0824418b..24ad8b8b4 100644 --- a/sql/updates/reverse-dev.sql +++ b/sql/updates/reverse-dev.sql @@ -154,7 +154,6 @@ GRANT SELECT ON _timescaledb_catalog.chunk TO PUBLIC; -- end recreate _timescaledb_catalog.chunk table -- - ALTER TABLE _timescaledb_internal.bgw_job_stat DROP CONSTRAINT bgw_job_stat_job_id_fkey; ALTER TABLE _timescaledb_internal.bgw_policy_chunk_stats @@ -246,3 +245,6 @@ CREATE FUNCTION @extschema@.alter_job( RETURNS TABLE (job_id INTEGER, schedule_interval INTERVAL, max_runtime INTERVAL, max_retries INTEGER, retry_period INTERVAL, scheduled BOOL, config JSONB, next_start TIMESTAMPTZ) AS '@MODULE_PATHNAME@', 'ts_job_alter' LANGUAGE C VOLATILE; + +DROP FUNCTION @extschema@.time_bucket(INTERVAL, TIMESTAMPTZ, TEXT, TIMESTAMPTZ, INTERVAL); + diff --git a/src/compat/compat.h b/src/compat/compat.h index 34c40df6d..7f7d44219 100644 --- a/src/compat/compat.h +++ b/src/compat/compat.h @@ -355,6 +355,11 @@ get_reindex_options(ReindexStmt *stmt) #define list_make5_int(x1, x2, x3, x4, x5) lappend_int(list_make4_int(x1, x2, x3, x4), x5) #endif +/* + * define lfifth macro for convenience + */ +#define lfifth(l) lfirst(list_nth_cell(l, 4)) + /* PG13 removes the natts parameter from map_variable_attnos */ #if PG13_LT #define map_variable_attnos_compat(node, varno, sublevels_up, map, natts, rowtype, found_wholerow) \ diff --git a/src/func_cache.c b/src/func_cache.c index ded672090..f1b05619e 100644 --- a/src/func_cache.c +++ b/src/func_cache.c @@ -20,6 +20,7 @@ #include #include +#include "compat/compat.h" #include "utils.h" #include "cache.h" #include "func_cache.h" @@ -108,6 +109,22 @@ time_bucket_sort_transform(FuncExpr *func) return do_sort_transform(func); } +/* + * time_bucket with timezone will always have 5 args. For the sort + * optimization to apply all args need to be Const except timestamp. + */ +static Expr * +time_bucket_tz_sort_transform(FuncExpr *func) +{ + Assert(list_length(func->args) == 5); + + if (!IsA(linitial((func)->args), Const) || !IsA(lthird(func->args), Const) || + !IsA(lfourth(func->args), Const) || !IsA(lfifth(func->args), Const)) + return (Expr *) func; + + return do_sort_transform(func); +} + /* For time_bucket this estimate currently works by seeing how many possible * buckets there will be if the data spans the entire hypertable. Note that * this is an overestimate. @@ -295,6 +312,16 @@ static FuncInfo funcinfo[] = { .group_estimate = time_bucket_group_estimate, .sort_transform = time_bucket_sort_transform, }, + { + .origin = ORIGIN_TIMESCALE, + .is_bucketing_func = true, + .allowed_in_cagg_definition = true, + .funcname = "time_bucket", + .nargs = 5, + .arg_types = { INTERVALOID, TIMESTAMPTZOID, TEXTOID, TIMESTAMPTZOID, INTERVALOID }, + .group_estimate = time_bucket_group_estimate, + .sort_transform = time_bucket_tz_sort_transform, + }, { .origin = ORIGIN_TIMESCALE_EXPERIMENTAL, .is_bucketing_func = true, diff --git a/src/planner/expand_hypertable.c b/src/planner/expand_hypertable.c index 19273f11f..2045401e9 100644 --- a/src/planner/expand_hypertable.c +++ b/src/planner/expand_hypertable.c @@ -434,6 +434,9 @@ transform_time_bucket_comparison(PlannerInfo *root, OpExpr *op) { Interval *interval = DatumGetIntervalP(width->constvalue); + /* + * Optimization can't be applied when interval has month component. + */ if (interval->month != 0) return op; @@ -466,7 +469,7 @@ transform_time_bucket_comparison(PlannerInfo *root, OpExpr *op) Assert(width->consttype == INTERVALOID); /* - * intervals with month component are not supported by time_bucket + * Optimization can't be applied when interval has month component. */ if (interval->month != 0) return op; @@ -513,7 +516,7 @@ transform_time_bucket_comparison(PlannerInfo *root, OpExpr *op) Assert(width->consttype == INTERVALOID); /* - * intervals with month component are not supported by time_bucket + * Optimization can't be applied when interval has month component. */ if (interval->month != 0) return op; diff --git a/src/time_bucket.c b/src/time_bucket.c index d99e0b460..14f0a8004 100644 --- a/src/time_bucket.c +++ b/src/time_bucket.c @@ -281,6 +281,64 @@ ts_timestamptz_bucket(PG_FUNCTION_ARGS) } } +TS_FUNCTION_INFO_V1(ts_timestamptz_timezone_bucket); + +/* + * time_bucket(bucket_width INTERVAL, ts TIMESTAMPTZ, timezone TEXT, origin TIMESTAMPTZ DEFAULT + * NULL, "offset" INTERVAL DEFAULT NULL) RETURNS TIMESTAMPTZ + */ +TSDLLEXPORT Datum +ts_timestamptz_timezone_bucket(PG_FUNCTION_ARGS) +{ + Datum period = PG_GETARG_DATUM(0); + Datum timestamp = PG_GETARG_DATUM(1); + Datum tzname = PG_GETARG_DATUM(2); + + /* + * When called from SQL we will always have 5 args because default values + * will be filled in for missing arguments. When called from C with + * DirectFunctionCall number of arguments might be less than 5. + */ + bool have_origin = PG_NARGS() > 3 && !PG_ARGISNULL(3); + bool have_offset = PG_NARGS() > 4 && !PG_ARGISNULL(4); + + /* + * We need to check for NULL arguments here because the function cannot be + * defined STRICT due to the optional arguments. + */ + if (PG_ARGISNULL(0) || PG_ARGISNULL(1) || PG_ARGISNULL(2)) + PG_RETURN_NULL(); + + /* Convert to local timestamp according to timezone */ + timestamp = DirectFunctionCall2(timestamptz_zone, tzname, timestamp); + if (have_offset) + { + /* Apply offset. */ + timestamp = DirectFunctionCall2(timestamp_mi_interval, timestamp, PG_GETARG_DATUM(4)); + } + + if (have_origin) + { + Datum origin = DirectFunctionCall2(timestamptz_zone, tzname, PG_GETARG_DATUM(3)); + timestamp = DirectFunctionCall3(ts_timestamp_bucket, period, timestamp, origin); + } + else + { + timestamp = DirectFunctionCall2(ts_timestamp_bucket, period, timestamp); + } + + if (have_offset) + { + /* Remove offset. */ + timestamp = DirectFunctionCall2(timestamp_pl_interval, timestamp, PG_GETARG_DATUM(4)); + } + + /* Convert back to timezone */ + timestamp = DirectFunctionCall2(timestamp_zone, tzname, timestamp); + + PG_RETURN_DATUM(timestamp); +} + static inline void check_period_is_daily(int64 period) { diff --git a/src/time_bucket.h b/src/time_bucket.h index f3637d389..6f4cbe585 100644 --- a/src/time_bucket.h +++ b/src/time_bucket.h @@ -17,6 +17,7 @@ extern TSDLLEXPORT Datum ts_int64_bucket(PG_FUNCTION_ARGS); extern TSDLLEXPORT Datum ts_date_bucket(PG_FUNCTION_ARGS); extern TSDLLEXPORT Datum ts_timestamp_bucket(PG_FUNCTION_ARGS); extern TSDLLEXPORT Datum ts_timestamptz_bucket(PG_FUNCTION_ARGS); +extern TSDLLEXPORT Datum ts_timestamptz_timezone_bucket(PG_FUNCTION_ARGS); extern TSDLLEXPORT int64 ts_time_bucket_by_type(int64 interval, int64 timestamp, Oid type); extern TSDLLEXPORT Datum ts_time_bucket_ng_date(PG_FUNCTION_ARGS); extern TSDLLEXPORT Datum ts_time_bucket_ng_timestamp(PG_FUNCTION_ARGS); diff --git a/src/ts_catalog/continuous_agg.c b/src/ts_catalog/continuous_agg.c index 7c079d88c..1c2b9495d 100644 --- a/src/ts_catalog/continuous_agg.c +++ b/src/ts_catalog/continuous_agg.c @@ -1541,46 +1541,88 @@ ts_continuous_agg_bucket_width(const ContinuousAgg *agg) * a common procedure used by ts_compute_* below. */ static Datum -generic_time_bucket_ng(const ContinuousAggsBucketFunction *bf, Datum timestamp) +generic_time_bucket(const ContinuousAggsBucketFunction *bf, Datum timestamp) { /* bf->timezone can't be NULL. If timezone is not specified, "" is stored */ Assert(bf->timezone != NULL); - if (strlen(bf->timezone) > 0) + if (!bf->experimental) { + if (strlen(bf->timezone) > 0) + { + if (TIMESTAMP_NOT_FINITE(bf->origin)) + { + /* using default origin */ + return DirectFunctionCall3(ts_timestamptz_timezone_bucket, + IntervalPGetDatum(bf->bucket_width), + timestamp, + CStringGetTextDatum(bf->timezone)); + } + else + { + /* custom origin specified */ + return DirectFunctionCall4(ts_timestamptz_timezone_bucket, + IntervalPGetDatum(bf->bucket_width), + timestamp, + CStringGetTextDatum(bf->timezone), + TimestampTzGetDatum((TimestampTz) bf->origin)); + } + } + if (TIMESTAMP_NOT_FINITE(bf->origin)) { /* using default origin */ - return DirectFunctionCall3(ts_time_bucket_ng_timezone, + return DirectFunctionCall2(ts_timestamp_bucket, IntervalPGetDatum(bf->bucket_width), - timestamp, - CStringGetTextDatum(bf->timezone)); + timestamp); } else { /* custom origin specified */ - return DirectFunctionCall4(ts_time_bucket_ng_timezone_origin, + return DirectFunctionCall3(ts_timestamp_bucket, IntervalPGetDatum(bf->bucket_width), timestamp, - TimestampTzGetDatum((TimestampTz) bf->origin), - CStringGetTextDatum(bf->timezone)); + TimestampGetDatum(bf->origin)); } } - - if (TIMESTAMP_NOT_FINITE(bf->origin)) - { - /* using default origin */ - return DirectFunctionCall2(ts_time_bucket_ng_timestamp, - IntervalPGetDatum(bf->bucket_width), - timestamp); - } else { - /* custom origin specified */ - return DirectFunctionCall3(ts_time_bucket_ng_timestamp, - IntervalPGetDatum(bf->bucket_width), - timestamp, - TimestampGetDatum(bf->origin)); + if (strlen(bf->timezone) > 0) + { + if (TIMESTAMP_NOT_FINITE(bf->origin)) + { + /* using default origin */ + return DirectFunctionCall3(ts_time_bucket_ng_timezone, + IntervalPGetDatum(bf->bucket_width), + timestamp, + CStringGetTextDatum(bf->timezone)); + } + else + { + /* custom origin specified */ + return DirectFunctionCall4(ts_time_bucket_ng_timezone_origin, + IntervalPGetDatum(bf->bucket_width), + timestamp, + TimestampTzGetDatum((TimestampTz) bf->origin), + CStringGetTextDatum(bf->timezone)); + } + } + + if (TIMESTAMP_NOT_FINITE(bf->origin)) + { + /* using default origin */ + return DirectFunctionCall2(ts_time_bucket_ng_timestamp, + IntervalPGetDatum(bf->bucket_width), + timestamp); + } + else + { + /* custom origin specified */ + return DirectFunctionCall3(ts_time_bucket_ng_timestamp, + IntervalPGetDatum(bf->bucket_width), + timestamp, + TimestampGetDatum(bf->origin)); + } } } @@ -1650,8 +1692,8 @@ ts_compute_inscribed_bucketed_refresh_window_variable(int64 *start, int64 *end, start_old = ts_internal_to_time_value(*start, TIMESTAMPOID); end_old = ts_internal_to_time_value(*end, TIMESTAMPOID); - start_new = generic_time_bucket_ng(bf, start_old); - end_new = generic_time_bucket_ng(bf, end_old); + start_new = generic_time_bucket(bf, start_old); + end_new = generic_time_bucket(bf, end_old); if (DatumGetTimestamp(start_new) != DatumGetTimestamp(start_old)) { @@ -1684,8 +1726,8 @@ ts_compute_circumscribed_bucketed_refresh_window_variable(int64 *start, int64 *e */ start_old = ts_internal_to_time_value(*start, TIMESTAMPOID); end_old = ts_internal_to_time_value(*end, TIMESTAMPOID); - start_new = generic_time_bucket_ng(bf, start_old); - end_new = generic_time_bucket_ng(bf, end_old); + start_new = generic_time_bucket(bf, start_old); + end_new = generic_time_bucket(bf, end_old); if (DatumGetTimestamp(end_new) != DatumGetTimestamp(end_old)) { @@ -1716,7 +1758,7 @@ ts_compute_beginning_of_the_next_bucket_variable(int64 timeval, */ val_old = ts_internal_to_time_value(timeval, TIMESTAMPOID); - val_new = generic_time_bucket_ng(bf, val_old); + val_new = generic_time_bucket(bf, val_old); val_new = generic_add_interval(bf, val_new); return ts_time_value_to_internal(val_new, TIMESTAMPOID); } diff --git a/test/expected/timestamp.out b/test/expected/timestamp.out index f76832d5e..e70e7a814 100644 --- a/test/expected/timestamp.out +++ b/test/expected/timestamp.out @@ -1284,6 +1284,117 @@ FROM generate_series('1990-01-03'::timestamptz,'1990-06-03'::timestamptz,'1month 1990-06-03 00:00:00-04 | 1990-05-31 20:00:00-04 | 1990-04-30 20:00:00-04 | 1990-03-31 19:00:00-05 | 1990-05-31 20:00:00-04 | 1990-05-31 20:00:00-04 | 1990-04-30 20:00:00-04 (6 rows) +--------------------------------------- +--- Test time_bucket with timezones --- +--------------------------------------- +-- test NULL args +SELECT +time_bucket(NULL::interval,now(),'Europe/Berlin'), +time_bucket('1day',NULL::timestamptz,'Europe/Berlin'), +time_bucket('1day',now(),NULL::text), +time_bucket('1day','2020-02-03','Europe/Berlin',NULL), +time_bucket('1day','2020-02-03','Europe/Berlin','2020-04-01',NULL), +time_bucket('1day','2020-02-03','Europe/Berlin',NULL,NULL), +time_bucket('1day','2020-02-03','Europe/Berlin',"offset":=NULL::interval), +time_bucket('1day','2020-02-03','Europe/Berlin',origin:=NULL::timestamptz); + time_bucket | time_bucket | time_bucket | time_bucket | time_bucket | time_bucket | time_bucket | time_bucket +-------------+-------------+-------------+------------------------+------------------------+------------------------+------------------------+------------------------ + | | | 2020-02-02 18:00:00-05 | 2020-02-03 00:00:00-05 | 2020-02-02 18:00:00-05 | 2020-02-02 18:00:00-05 | 2020-02-02 18:00:00-05 +(1 row) + +SET datestyle TO ISO; +SELECT + time_bucket('1day', ts) AS "UTC", + time_bucket('1day', ts, 'Europe/Berlin') AS "Berlin", + time_bucket('1day', ts, 'Europe/London') AS "London", + time_bucket('1day', ts, 'America/New_York') AS "New York", + time_bucket('1day', ts, 'PST') AS "PST", + time_bucket('1day', ts, current_setting('timezone')) AS "current" +FROM generate_series('1999-12-31 17:00'::timestamptz,'2000-01-02 3:00'::timestamptz, '1hour'::interval) ts; + UTC | Berlin | London | New York | PST | current +------------------------+------------------------+------------------------+------------------------+------------------------+------------------------ + 1999-12-30 19:00:00-05 | 1999-12-30 18:00:00-05 | 1999-12-30 19:00:00-05 | 1999-12-31 00:00:00-05 | 1999-12-31 03:00:00-05 | 1999-12-31 00:00:00-05 + 1999-12-30 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-30 19:00:00-05 | 1999-12-31 00:00:00-05 | 1999-12-31 03:00:00-05 | 1999-12-31 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 1999-12-31 00:00:00-05 | 1999-12-31 03:00:00-05 | 1999-12-31 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 1999-12-31 00:00:00-05 | 1999-12-31 03:00:00-05 | 1999-12-31 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 1999-12-31 00:00:00-05 | 1999-12-31 03:00:00-05 | 1999-12-31 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 1999-12-31 00:00:00-05 | 1999-12-31 03:00:00-05 | 1999-12-31 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 1999-12-31 00:00:00-05 | 1999-12-31 03:00:00-05 | 1999-12-31 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 1999-12-31 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 1999-12-31 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 1999-12-31 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 1999-12-31 19:00:00-05 | 2000-01-01 18:00:00-05 | 1999-12-31 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 2000-01-01 19:00:00-05 | 2000-01-01 18:00:00-05 | 2000-01-01 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 2000-01-01 19:00:00-05 | 2000-01-01 18:00:00-05 | 2000-01-01 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 2000-01-01 19:00:00-05 | 2000-01-01 18:00:00-05 | 2000-01-01 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 2000-01-01 19:00:00-05 | 2000-01-01 18:00:00-05 | 2000-01-01 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 2000-01-01 19:00:00-05 | 2000-01-01 18:00:00-05 | 2000-01-01 19:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-01 00:00:00-05 + 2000-01-01 19:00:00-05 | 2000-01-01 18:00:00-05 | 2000-01-01 19:00:00-05 | 2000-01-02 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-02 00:00:00-05 + 2000-01-01 19:00:00-05 | 2000-01-01 18:00:00-05 | 2000-01-01 19:00:00-05 | 2000-01-02 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-02 00:00:00-05 + 2000-01-01 19:00:00-05 | 2000-01-01 18:00:00-05 | 2000-01-01 19:00:00-05 | 2000-01-02 00:00:00-05 | 2000-01-01 03:00:00-05 | 2000-01-02 00:00:00-05 + 2000-01-01 19:00:00-05 | 2000-01-01 18:00:00-05 | 2000-01-01 19:00:00-05 | 2000-01-02 00:00:00-05 | 2000-01-02 03:00:00-05 | 2000-01-02 00:00:00-05 +(35 rows) + +SELECT + time_bucket('1month', ts) AS "UTC", + time_bucket('1month', ts, 'Europe/Berlin') AS "Berlin", + time_bucket('1month', ts, 'America/New_York') AS "New York", + time_bucket('1month', ts, current_setting('timezone')) AS "current", + time_bucket('2month', ts, current_setting('timezone')) AS "2m", + time_bucket('2month', ts, current_setting('timezone'), '2000-02-01'::timestamp) AS "2m origin", + time_bucket('2month', ts, current_setting('timezone'), "offset":='14 day'::interval) AS "2m offset", + time_bucket('2month', ts, current_setting('timezone'), '2000-02-01'::timestamp, '7 day'::interval) AS "2m offset + origin" +FROM generate_series('1999-12-01'::timestamptz,'2000-09-01'::timestamptz, '9 day'::interval) ts; + UTC | Berlin | New York | current | 2m | 2m origin | 2m offset | 2m offset + origin +------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------+------------------------ + 1999-11-30 19:00:00-05 | 1999-11-30 18:00:00-05 | 1999-12-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 1999-11-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 1999-11-15 00:00:00-05 | 1999-10-08 00:00:00-04 + 1999-11-30 19:00:00-05 | 1999-11-30 18:00:00-05 | 1999-12-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 1999-11-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 1999-11-15 00:00:00-05 | 1999-12-08 00:00:00-05 + 1999-11-30 19:00:00-05 | 1999-11-30 18:00:00-05 | 1999-12-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 1999-11-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 1999-11-15 00:00:00-05 | 1999-12-08 00:00:00-05 + 1999-11-30 19:00:00-05 | 1999-11-30 18:00:00-05 | 1999-12-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 1999-11-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 1999-11-15 00:00:00-05 | 1999-12-08 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 1999-11-15 00:00:00-05 | 1999-12-08 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 2000-01-15 00:00:00-05 | 1999-12-08 00:00:00-05 + 1999-12-31 19:00:00-05 | 1999-12-31 18:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 1999-12-01 00:00:00-05 | 2000-01-15 00:00:00-05 | 1999-12-08 00:00:00-05 + 2000-01-31 19:00:00-05 | 2000-01-31 18:00:00-05 | 2000-02-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-01-15 00:00:00-05 | 1999-12-08 00:00:00-05 + 2000-01-31 19:00:00-05 | 2000-01-31 18:00:00-05 | 2000-02-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-01-15 00:00:00-05 | 2000-02-08 00:00:00-05 + 2000-01-31 19:00:00-05 | 2000-01-31 18:00:00-05 | 2000-02-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-01-15 00:00:00-05 | 2000-02-08 00:00:00-05 + 2000-01-31 19:00:00-05 | 2000-01-31 18:00:00-05 | 2000-02-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-01-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-01-15 00:00:00-05 | 2000-02-08 00:00:00-05 + 2000-02-29 19:00:00-05 | 2000-02-29 18:00:00-05 | 2000-03-01 00:00:00-05 | 2000-03-01 00:00:00-05 | 2000-03-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-01-15 00:00:00-05 | 2000-02-08 00:00:00-05 + 2000-02-29 19:00:00-05 | 2000-02-29 18:00:00-05 | 2000-03-01 00:00:00-05 | 2000-03-01 00:00:00-05 | 2000-03-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-03-15 00:00:00-05 | 2000-02-08 00:00:00-05 + 2000-02-29 19:00:00-05 | 2000-02-29 18:00:00-05 | 2000-03-01 00:00:00-05 | 2000-03-01 00:00:00-05 | 2000-03-01 00:00:00-05 | 2000-02-01 00:00:00-05 | 2000-03-15 00:00:00-05 | 2000-02-08 00:00:00-05 + 2000-03-31 19:00:00-05 | 2000-03-31 17:00:00-05 | 2000-04-01 00:00:00-05 | 2000-04-01 00:00:00-05 | 2000-03-01 00:00:00-05 | 2000-04-01 00:00:00-05 | 2000-03-15 00:00:00-05 | 2000-02-08 00:00:00-05 + 2000-03-31 19:00:00-05 | 2000-03-31 17:00:00-05 | 2000-04-01 00:00:00-05 | 2000-04-01 00:00:00-05 | 2000-03-01 00:00:00-05 | 2000-04-01 00:00:00-05 | 2000-03-15 00:00:00-05 | 2000-04-08 00:00:00-04 + 2000-03-31 19:00:00-05 | 2000-03-31 17:00:00-05 | 2000-04-01 00:00:00-05 | 2000-04-01 00:00:00-05 | 2000-03-01 00:00:00-05 | 2000-04-01 00:00:00-05 | 2000-03-15 00:00:00-05 | 2000-04-08 00:00:00-04 + 2000-04-30 20:00:00-04 | 2000-04-30 18:00:00-04 | 2000-05-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-04-01 00:00:00-05 | 2000-03-15 00:00:00-05 | 2000-04-08 00:00:00-04 + 2000-04-30 20:00:00-04 | 2000-04-30 18:00:00-04 | 2000-05-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-04-01 00:00:00-05 | 2000-03-15 00:00:00-05 | 2000-04-08 00:00:00-04 + 2000-04-30 20:00:00-04 | 2000-04-30 18:00:00-04 | 2000-05-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-04-01 00:00:00-05 | 2000-05-15 00:00:00-04 | 2000-04-08 00:00:00-04 + 2000-04-30 20:00:00-04 | 2000-04-30 18:00:00-04 | 2000-05-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-04-01 00:00:00-05 | 2000-05-15 00:00:00-04 | 2000-04-08 00:00:00-04 + 2000-05-31 20:00:00-04 | 2000-05-31 18:00:00-04 | 2000-06-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-05-15 00:00:00-04 | 2000-04-08 00:00:00-04 + 2000-05-31 20:00:00-04 | 2000-05-31 18:00:00-04 | 2000-06-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-05-15 00:00:00-04 | 2000-06-08 00:00:00-04 + 2000-05-31 20:00:00-04 | 2000-05-31 18:00:00-04 | 2000-06-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-05-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-05-15 00:00:00-04 | 2000-06-08 00:00:00-04 + 2000-06-30 20:00:00-04 | 2000-06-30 18:00:00-04 | 2000-07-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-05-15 00:00:00-04 | 2000-06-08 00:00:00-04 + 2000-06-30 20:00:00-04 | 2000-06-30 18:00:00-04 | 2000-07-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-05-15 00:00:00-04 | 2000-06-08 00:00:00-04 + 2000-06-30 20:00:00-04 | 2000-06-30 18:00:00-04 | 2000-07-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-07-15 00:00:00-04 | 2000-06-08 00:00:00-04 + 2000-06-30 20:00:00-04 | 2000-06-30 18:00:00-04 | 2000-07-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-06-01 00:00:00-04 | 2000-07-15 00:00:00-04 | 2000-06-08 00:00:00-04 + 2000-07-31 20:00:00-04 | 2000-07-31 18:00:00-04 | 2000-08-01 00:00:00-04 | 2000-08-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-08-01 00:00:00-04 | 2000-07-15 00:00:00-04 | 2000-08-08 00:00:00-04 + 2000-07-31 20:00:00-04 | 2000-07-31 18:00:00-04 | 2000-08-01 00:00:00-04 | 2000-08-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-08-01 00:00:00-04 | 2000-07-15 00:00:00-04 | 2000-08-08 00:00:00-04 + 2000-07-31 20:00:00-04 | 2000-07-31 18:00:00-04 | 2000-08-01 00:00:00-04 | 2000-08-01 00:00:00-04 | 2000-07-01 00:00:00-04 | 2000-08-01 00:00:00-04 | 2000-07-15 00:00:00-04 | 2000-08-08 00:00:00-04 +(31 rows) + RESET datestyle; ------------------------------------------------------------ --- Test timescaledb_experimental.time_bucket_ng function -- diff --git a/test/sql/timestamp.sql b/test/sql/timestamp.sql index 7dc114deb..05ce0e645 100644 --- a/test/sql/timestamp.sql +++ b/test/sql/timestamp.sql @@ -634,6 +634,44 @@ SELECT time_bucket('3 month', time, '2000-02-01'::timestamptz) AS "3m origin" FROM generate_series('1990-01-03'::timestamptz,'1990-06-03'::timestamptz,'1month'::interval) time; +--------------------------------------- +--- Test time_bucket with timezones --- +--------------------------------------- + +-- test NULL args +SELECT +time_bucket(NULL::interval,now(),'Europe/Berlin'), +time_bucket('1day',NULL::timestamptz,'Europe/Berlin'), +time_bucket('1day',now(),NULL::text), +time_bucket('1day','2020-02-03','Europe/Berlin',NULL), +time_bucket('1day','2020-02-03','Europe/Berlin','2020-04-01',NULL), +time_bucket('1day','2020-02-03','Europe/Berlin',NULL,NULL), +time_bucket('1day','2020-02-03','Europe/Berlin',"offset":=NULL::interval), +time_bucket('1day','2020-02-03','Europe/Berlin',origin:=NULL::timestamptz); + +SET datestyle TO ISO; +SELECT + time_bucket('1day', ts) AS "UTC", + time_bucket('1day', ts, 'Europe/Berlin') AS "Berlin", + time_bucket('1day', ts, 'Europe/London') AS "London", + time_bucket('1day', ts, 'America/New_York') AS "New York", + time_bucket('1day', ts, 'PST') AS "PST", + time_bucket('1day', ts, current_setting('timezone')) AS "current" + +FROM generate_series('1999-12-31 17:00'::timestamptz,'2000-01-02 3:00'::timestamptz, '1hour'::interval) ts; + +SELECT + time_bucket('1month', ts) AS "UTC", + time_bucket('1month', ts, 'Europe/Berlin') AS "Berlin", + time_bucket('1month', ts, 'America/New_York') AS "New York", + time_bucket('1month', ts, current_setting('timezone')) AS "current", + time_bucket('2month', ts, current_setting('timezone')) AS "2m", + time_bucket('2month', ts, current_setting('timezone'), '2000-02-01'::timestamp) AS "2m origin", + time_bucket('2month', ts, current_setting('timezone'), "offset":='14 day'::interval) AS "2m offset", + time_bucket('2month', ts, current_setting('timezone'), '2000-02-01'::timestamp, '7 day'::interval) AS "2m offset + origin" + +FROM generate_series('1999-12-01'::timestamptz,'2000-09-01'::timestamptz, '9 day'::interval) ts; + RESET datestyle; ------------------------------------------------------------ diff --git a/tsl/src/continuous_aggs/create.c b/tsl/src/continuous_aggs/create.c index 0cfab27b2..298aed1f4 100644 --- a/tsl/src/continuous_aggs/create.c +++ b/tsl/src/continuous_aggs/create.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -184,6 +185,7 @@ typedef struct CAggTimebucketInfo Interval *interval; /* stores the interval, NULL if not specified */ const char *timezone; /* the name of the timezone, NULL if not specified */ + FuncExpr *bucket_func; /* function call expr of the bucketing function */ /* * Custom origin value stored as UTC timestamp. * If not specified, stores infinity. @@ -712,6 +714,24 @@ caggtimebucketinfo_init(CAggTimebucketInfo *src, int32 hypertable_id, Oid hypert TIMESTAMP_NOBEGIN(src->origin); /* origin is not specified by default */ } +static Const * +check_time_bucket_argument(Node *arg, char *position) +{ + if (IsA(arg, NamedArgExpr)) + arg = (Node *) castNode(NamedArgExpr, arg)->arg; + + Node *expr = eval_const_expressions(NULL, arg); + + if (!IsA(expr, Const)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only immutable expressions allowed in time bucket function"), + errhint("Use an immutable expression as %s argument to the time bucket function.", + position))); + + return castNode(Const, expr); +} + /* * Check if the group-by clauses has exactly 1 time_bucket(.., ) where * is the hypertable's partitioning column and other invariants. Then fill @@ -722,6 +742,7 @@ caggtimebucket_validate(CAggTimebucketInfo *tbinfo, List *groupClause, List *tar { ListCell *l; bool found = false; + bool custom_origin = false; /* Make sure tbinfo was initialized. This assumption is used below. */ Assert(tbinfo->bucket_width == 0); @@ -737,11 +758,18 @@ caggtimebucket_validate(CAggTimebucketInfo *tbinfo, List *groupClause, List *tar FuncExpr *fe = ((FuncExpr *) tle->expr); Node *width_arg; Node *col_arg; - Node *tz_arg; if (!function_allowed_in_cagg_definition(fe->funcid)) continue; + /* + * offset variants of time_bucket functions are not + * supported at the moment. + */ + if (list_length(fe->args) >= 5 || + (list_length(fe->args) == 4 && exprType(lfourth(fe->args)) == INTERVALOID)) + continue; + if (found) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -750,6 +778,8 @@ caggtimebucket_validate(CAggTimebucketInfo *tbinfo, List *groupClause, List *tar else found = true; + tbinfo->bucket_func = fe; + /*only column allowed : time_bucket('1day', ) */ col_arg = lsecond(fe->args); @@ -759,54 +789,12 @@ caggtimebucket_validate(CAggTimebucketInfo *tbinfo, List *groupClause, List *tar errmsg( "time bucket function must reference a hypertable dimension column"))); - if (list_length(fe->args) == 4) - { - /* - * Timezone and custom origin are specified. In this clause we - * save only the timezone. Origin is processed in the following - * clause. - */ - tz_arg = eval_const_expressions(NULL, lfourth(fe->args)); - - if (!IsA(tz_arg, Const)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("only immutable expressions allowed in time bucket function"), - errhint("Use an immutable expression as fourth argument" - " to the time bucket function."))); - - Const *tz = castNode(Const, tz_arg); - - /* This is assured by function_allowed_in_cagg_definition() above. */ - Assert(tz->consttype == TEXTOID); - const char *tz_name = TextDatumGetCString(tz->constvalue); - if (!ts_is_valid_timezone_name(tz_name)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid timezone name \"%s\"", tz_name))); - } - - tbinfo->timezone = tz_name; - tbinfo->bucket_width = BUCKET_WIDTH_VARIABLE; - } - if (list_length(fe->args) >= 3) { - tz_arg = eval_const_expressions(NULL, lthird(fe->args)); - if (!IsA(tz_arg, Const)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("only immutable expressions allowed in time bucket function"), - errhint("Use an immutable expression as third argument" - " to the time bucket function."))); - - Const *tz = castNode(Const, tz_arg); - if ((tz->consttype == TEXTOID) && (list_length(fe->args) == 3)) + Const *arg = check_time_bucket_argument(lthird(fe->args), "third"); + if (exprType((Node *) arg) == TEXTOID) { - /* Timezone specified */ - const char *tz_name = TextDatumGetCString(tz->constvalue); - + const char *tz_name = TextDatumGetCString(arg->constvalue); if (!ts_is_valid_timezone_name(tz_name)) { ereport(ERROR, @@ -817,53 +805,71 @@ caggtimebucket_validate(CAggTimebucketInfo *tbinfo, List *groupClause, List *tar tbinfo->timezone = tz_name; tbinfo->bucket_width = BUCKET_WIDTH_VARIABLE; } - else + } + + if (list_length(fe->args) >= 4) + { + Const *arg = check_time_bucket_argument(lfourth(fe->args), "fourth"); + if (exprType((Node *) arg) == TEXTOID) { - /* - * Custom origin specified. This is always treated as - * a variable-sized bucket case. - */ + const char *tz_name = TextDatumGetCString(arg->constvalue); + if (!ts_is_valid_timezone_name(tz_name)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid timezone name \"%s\"", tz_name))); + } + + tbinfo->timezone = tz_name; tbinfo->bucket_width = BUCKET_WIDTH_VARIABLE; - - if (tz->constisnull) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid origin value: null"))); - } - - switch (tz->consttype) - { - case DATEOID: - tbinfo->origin = DatumGetTimestamp( - DirectFunctionCall1(date_timestamp, tz->constvalue)); - break; - case TIMESTAMPOID: - tbinfo->origin = DatumGetTimestamp(tz->constvalue); - break; - case TIMESTAMPTZOID: - tbinfo->origin = DatumGetTimestampTz(tz->constvalue); - break; - default: - /* - * This shouldn't happen. But if somehow it does - * make sure the execution will stop here even in - * the Release build. - */ - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("unsupported time bucket function"))); - } - - if (TIMESTAMP_NOT_FINITE(tbinfo->origin)) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid origin value: infinity"))); - } } } + /* check for custom origin */ + switch (exprType(col_arg)) + { + case DATEOID: + /* origin is always 3rd arg for date variants */ + if (list_length(fe->args) == 3) + { + custom_origin = true; + tbinfo->origin = DatumGetTimestamp( + DirectFunctionCall1(date_timestamp, + castNode(Const, lthird(fe->args))->constvalue)); + } + break; + case TIMESTAMPOID: + /* origin is always 3rd arg for timestamp variants */ + if (list_length(fe->args) == 3) + { + custom_origin = true; + tbinfo->origin = + DatumGetTimestamp(castNode(Const, lthird(fe->args))->constvalue); + } + break; + case TIMESTAMPTZOID: + /* origin can be 3rd or 4th arg for timestamptz variants */ + if (list_length(fe->args) >= 3 && exprType(lthird(fe->args)) == TIMESTAMPTZOID) + { + custom_origin = true; + tbinfo->origin = + DatumGetTimestampTz(castNode(Const, lthird(fe->args))->constvalue); + } + else if (list_length(fe->args) >= 4 && + exprType(lfourth(fe->args)) == TIMESTAMPTZOID) + { + custom_origin = true; + tbinfo->origin = + DatumGetTimestampTz(castNode(Const, lfourth(fe->args))->constvalue); + } + } + if (custom_origin && TIMESTAMP_NOT_FINITE(tbinfo->origin)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid origin value: infinity"))); + } + /* * We constify width expression here so any immutable expression will be allowed * otherwise it would make it harder to create caggs for hypertables with e.g. int8 @@ -895,35 +901,9 @@ caggtimebucket_validate(CAggTimebucketInfo *tbinfo, List *groupClause, List *tar errhint("Use an immutable expression as first argument" " to the time bucket function."))); - if ((tbinfo->bucket_width == BUCKET_WIDTH_VARIABLE) && (tbinfo->interval->month != 0)) + if (tbinfo->interval && tbinfo->interval->month) { - /* Monthly buckets case */ - if (!TIMESTAMP_NOT_FINITE(tbinfo->origin)) - { - /* - * Origin was specified - make sure it's the first day of the month. - * If a timezone was specified the check should be done in this timezone. - */ - Timestamp origin = tbinfo->origin; - if (tbinfo->timezone != NULL) - { - /* The code is equal to 'timestamptz AT TIME ZONE tzname'. */ - origin = DatumGetTimestamp( - DirectFunctionCall2(timestamptz_zone, - CStringGetTextDatum(tbinfo->timezone), - TimestampTzGetDatum((TimestampTz) origin))); - } - - const char *day = - TextDatumGetCString(DirectFunctionCall2(timestamp_to_char, - TimestampGetDatum(origin), - CStringGetTextDatum("DD"))); - if (strcmp(day, "01") != 0) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("for monthly buckets origin must be the first day of the " - "month"))); - } + tbinfo->bucket_width = BUCKET_WIDTH_VARIABLE; } } } @@ -2312,18 +2292,17 @@ cagg_create(const CreateTableAsStmt *create_stmt, ViewStmt *stmt, Query *panquer } /* - * `experimental` = true and `name` = "time_bucket_ng" are hardcoded - * rather than extracted from the query. We happen to know that - * monthly buckets can currently be created only with time_bucket_ng(), - * thus these values are correct. Besides, they are not used for + * These values are not used for * anything except Assert's yet for the same reasons. Once the design * of variable-sized buckets is finalized we will have a better idea * of what schema is needed exactly. Until then the choice was made * in favor of the most generic schema that can be optimized later. */ create_bucket_function_catalog_entry(materialize_hypertable_id, - true, - "time_bucket_ng", + get_func_namespace( + origquery_ht->bucket_func->funcid) != + PG_PUBLIC_NAMESPACE, + get_func_name(origquery_ht->bucket_func->funcid), bucket_width, origin, origquery_ht->timezone); diff --git a/tsl/test/expected/cagg_usage.out b/tsl/test/expected/cagg_usage.out index 229a934c5..cc5800d27 100644 --- a/tsl/test/expected/cagg_usage.out +++ b/tsl/test/expected/cagg_usage.out @@ -410,3 +410,58 @@ SELECT relname FROM pg_class WHERE oid = :mat_table; -- Cleanup DROP TABLE whatever; -- END OF BASIC USAGE TESTS -- +CREATE TABLE metrics(time timestamptz, device TEXT, value float); +SELECT table_name FROM create_hypertable('metrics','time'); +NOTICE: adding not-null constraint to column "time" + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics SELECT generate_series('1999-12-20'::timestamptz,'2000-02-01'::timestamptz,'12 day'::interval), 'dev1', 0.25; +SELECT current_setting('timezone'); + current_setting +----------------- + PST8PDT +(1 row) + +-- should be blocked because non-immutable expression +\set ON_ERROR_STOP 0 +CREATE MATERIALIZED VIEW cagg1 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 day', time, current_setting('timezone')) FROM metrics GROUP BY 1; +ERROR: only immutable expressions allowed in time bucket function +\set ON_ERROR_STOP 1 +CREATE MATERIALIZED VIEW cagg1 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 day', time, 'PST8PDT') FROM metrics GROUP BY 1; +NOTICE: refreshing continuous aggregate "cagg1" +SELECT * FROM cagg1; + time_bucket +------------------------------ + Mon Dec 20 00:00:00 1999 PST + Sat Jan 01 00:00:00 2000 PST + Thu Jan 13 00:00:00 2000 PST + Tue Jan 25 00:00:00 2000 PST +(4 rows) + +CREATE MATERIALIZED VIEW cagg2 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 month', time, 'PST8PDT') FROM metrics GROUP BY 1; +NOTICE: refreshing continuous aggregate "cagg2" +SELECT * FROM cagg2; + time_bucket +------------------------------ + Wed Dec 01 00:00:00 1999 PST + Sat Jan 01 00:00:00 2000 PST +(2 rows) + +-- custom origin +CREATE MATERIALIZED VIEW cagg3 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 month', time, 'PST8PDT', '2000-01-01'::timestamptz) FROM metrics GROUP BY 1; +NOTICE: refreshing continuous aggregate "cagg3" +SELECT * FROM cagg3; + time_bucket +------------------------------ + Wed Dec 01 00:00:00 1999 PST + Sat Jan 01 00:00:00 2000 PST +(2 rows) + +-- offset not supported atm +\set ON_ERROR_STOP 0 +CREATE MATERIALIZED VIEW cagg4 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 month', time, 'PST8PDT', "offset":= INTERVAL '15 day') FROM metrics GROUP BY 1; +ERROR: continuous aggregate view must include a valid time bucket function +\set ON_ERROR_STOP 1 diff --git a/tsl/test/expected/exp_cagg_origin.out b/tsl/test/expected/exp_cagg_origin.out index 6f75c3596..d15ea9628 100644 --- a/tsl/test/expected/exp_cagg_origin.out +++ b/tsl/test/expected/exp_cagg_origin.out @@ -30,17 +30,6 @@ INSERT INTO conditions (day, city, temperature) VALUES ('2021-06-26', 'Moscow', 32), ('2021-06-27', 'Moscow', 31); \set ON_ERROR_STOP 0 --- Make sure NULL can't be specified as an origin -CREATE MATERIALIZED VIEW conditions_summary_weekly -WITH (timescaledb.continuous, timescaledb.materialized_only=true) AS -SELECT city, - timescaledb_experimental.time_bucket_ng('7 days', day, null) AS bucket, - MIN(temperature), - MAX(temperature) -FROM conditions -GROUP BY city, bucket -WITH NO DATA; -ERROR: invalid origin value: null -- Make sure 'infinity' can't be specified as an origin CREATE MATERIALIZED VIEW conditions_summary_weekly WITH (timescaledb.continuous, timescaledb.materialized_only=true) AS @@ -52,17 +41,6 @@ FROM conditions GROUP BY city, bucket WITH NO DATA; ERROR: invalid origin value: infinity --- For monthly buckets origin should be the first day of the month -CREATE MATERIALIZED VIEW conditions_summary_weekly -WITH (timescaledb.continuous, timescaledb.materialized_only=true) AS -SELECT city, - timescaledb_experimental.time_bucket_ng('1 month', day, '2021-06-03') AS bucket, - MIN(temperature), - MAX(temperature) -FROM conditions -GROUP BY city, bucket -WITH NO DATA; -ERROR: for monthly buckets origin must be the first day of the month -- Make sure buckets like '1 months 15 days" (fixed+variable-sized) are not allowed CREATE MATERIALIZED VIEW conditions_summary_weekly WITH (timescaledb.continuous, timescaledb.materialized_only=true) AS @@ -95,24 +73,6 @@ SELECT mat_hypertable_id AS cagg_id, raw_hypertable_id AS ht_id FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'conditions_summary_weekly' \gset --- Make sure this is treated as a variable-sized bucket case -SELECT bucket_width -FROM _timescaledb_catalog.continuous_agg -WHERE mat_hypertable_id = :cagg_id; - bucket_width --------------- - -1 -(1 row) - --- Make sure the origin is saved in the catalog table -SELECT experimental, name, bucket_width, origin, timezone -FROM _timescaledb_catalog.continuous_aggs_bucket_function -WHERE mat_hypertable_id = :cagg_id; - experimental | name | bucket_width | origin | timezone ---------------+----------------+--------------+--------------------------+---------- - t | time_bucket_ng | @ 7 days | Mon Jan 03 00:00:00 2000 | -(1 row) - -- Make sure truncating of the refresh window works \set ON_ERROR_STOP 0 CALL refresh_continuous_aggregate('conditions_summary_weekly', '2021-06-14', '2021-06-20'); @@ -955,7 +915,7 @@ SELECT city, MAX(temperature) FROM conditions_timestamptz GROUP BY city, bucket; -ERROR: for monthly buckets origin must be the first day of the month +ERROR: origin must be the first day of the month -- Make sure buckets like '1 months 15 days" (fixed+variable-sized) are not allowed CREATE MATERIALIZED VIEW conditions_summary_timestamptz WITH (timescaledb.continuous, timescaledb.materialized_only=true) AS @@ -1139,7 +1099,7 @@ SELECT add_continuous_aggregate_policy('conditions_summary_timestamptz', DROP TABLE conditions_timestamptz CASCADE; NOTICE: drop cascades to 3 other objects NOTICE: drop cascades to 3 other objects -NOTICE: drop cascades to table _timescaledb_internal._hyper_18_248_chunk +NOTICE: drop cascades to table _timescaledb_internal._hyper_19_248_chunk \c :TEST_DBNAME :ROLE_CLUSTER_SUPERUSER DROP DATABASE :DATA_NODE_1; DROP DATABASE :DATA_NODE_2; diff --git a/tsl/test/shared/expected/extension.out b/tsl/test/shared/expected/extension.out index ecd0832dc..7542f8453 100644 --- a/tsl/test/shared/expected/extension.out +++ b/tsl/test/shared/expected/extension.out @@ -196,6 +196,7 @@ ORDER BY pronamespace::regnamespace::text COLLATE "C", p.oid::regprocedure::text time_bucket(interval,date,interval) time_bucket(interval,timestamp with time zone) time_bucket(interval,timestamp with time zone,interval) + time_bucket(interval,timestamp with time zone,text,timestamp with time zone,interval) time_bucket(interval,timestamp with time zone,timestamp with time zone) time_bucket(interval,timestamp without time zone) time_bucket(interval,timestamp without time zone,interval) diff --git a/tsl/test/sql/cagg_usage.sql b/tsl/test/sql/cagg_usage.sql index 1ed0b15d3..ff5b9cc47 100644 --- a/tsl/test/sql/cagg_usage.sql +++ b/tsl/test/sql/cagg_usage.sql @@ -268,3 +268,30 @@ SELECT relname FROM pg_class WHERE oid = :mat_table; DROP TABLE whatever; -- END OF BASIC USAGE TESTS -- + +CREATE TABLE metrics(time timestamptz, device TEXT, value float); +SELECT table_name FROM create_hypertable('metrics','time'); +INSERT INTO metrics SELECT generate_series('1999-12-20'::timestamptz,'2000-02-01'::timestamptz,'12 day'::interval), 'dev1', 0.25; + +SELECT current_setting('timezone'); + +-- should be blocked because non-immutable expression +\set ON_ERROR_STOP 0 +CREATE MATERIALIZED VIEW cagg1 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 day', time, current_setting('timezone')) FROM metrics GROUP BY 1; +\set ON_ERROR_STOP 1 + +CREATE MATERIALIZED VIEW cagg1 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 day', time, 'PST8PDT') FROM metrics GROUP BY 1; +SELECT * FROM cagg1; + +CREATE MATERIALIZED VIEW cagg2 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 month', time, 'PST8PDT') FROM metrics GROUP BY 1; +SELECT * FROM cagg2; + +-- custom origin +CREATE MATERIALIZED VIEW cagg3 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 month', time, 'PST8PDT', '2000-01-01'::timestamptz) FROM metrics GROUP BY 1; +SELECT * FROM cagg3; + +-- offset not supported atm +\set ON_ERROR_STOP 0 +CREATE MATERIALIZED VIEW cagg4 WITH (timescaledb.continuous,timescaledb.materialized_only=true) AS SELECT time_bucket('1 month', time, 'PST8PDT', "offset":= INTERVAL '15 day') FROM metrics GROUP BY 1; +\set ON_ERROR_STOP 1 + diff --git a/tsl/test/sql/exp_cagg_origin.sql b/tsl/test/sql/exp_cagg_origin.sql index bc88c3c4d..450ee126b 100644 --- a/tsl/test/sql/exp_cagg_origin.sql +++ b/tsl/test/sql/exp_cagg_origin.sql @@ -30,17 +30,6 @@ INSERT INTO conditions (day, city, temperature) VALUES \set ON_ERROR_STOP 0 --- Make sure NULL can't be specified as an origin -CREATE MATERIALIZED VIEW conditions_summary_weekly -WITH (timescaledb.continuous, timescaledb.materialized_only=true) AS -SELECT city, - timescaledb_experimental.time_bucket_ng('7 days', day, null) AS bucket, - MIN(temperature), - MAX(temperature) -FROM conditions -GROUP BY city, bucket -WITH NO DATA; - -- Make sure 'infinity' can't be specified as an origin CREATE MATERIALIZED VIEW conditions_summary_weekly WITH (timescaledb.continuous, timescaledb.materialized_only=true) AS @@ -52,17 +41,6 @@ FROM conditions GROUP BY city, bucket WITH NO DATA; --- For monthly buckets origin should be the first day of the month -CREATE MATERIALIZED VIEW conditions_summary_weekly -WITH (timescaledb.continuous, timescaledb.materialized_only=true) AS -SELECT city, - timescaledb_experimental.time_bucket_ng('1 month', day, '2021-06-03') AS bucket, - MIN(temperature), - MAX(temperature) -FROM conditions -GROUP BY city, bucket -WITH NO DATA; - -- Make sure buckets like '1 months 15 days" (fixed+variable-sized) are not allowed CREATE MATERIALIZED VIEW conditions_summary_weekly WITH (timescaledb.continuous, timescaledb.materialized_only=true) AS @@ -95,16 +73,6 @@ FROM _timescaledb_catalog.continuous_agg WHERE user_view_name = 'conditions_summary_weekly' \gset --- Make sure this is treated as a variable-sized bucket case -SELECT bucket_width -FROM _timescaledb_catalog.continuous_agg -WHERE mat_hypertable_id = :cagg_id; - --- Make sure the origin is saved in the catalog table -SELECT experimental, name, bucket_width, origin, timezone -FROM _timescaledb_catalog.continuous_aggs_bucket_function -WHERE mat_hypertable_id = :cagg_id; - -- Make sure truncating of the refresh window works \set ON_ERROR_STOP 0 CALL refresh_continuous_aggregate('conditions_summary_weekly', '2021-06-14', '2021-06-20');