Repository: timescale/timescaledb (2025-08-28T23:08:57Z)

Issue:
[Bug]: Continuous aggregates with time_bucket hourly and timezone fail to update
### What type of bug is this?

Incorrect result, Unexpected error

### What subsystems and features are affected?

Continuous aggregate

### What happened?

I have a number of Continuous aggregates that uses hourly buckets
```
 time_bucket(interval '1 hour', hypertable_name.time, 'Asia/Dubai')  AS bucket
 ```
 
 I noticed that i stopped seeing the new results after recreating the hypertable. After hours of debugging i pinned down that the hourly buckets dont refresh when used with timezone
 
 
![image](https://github.com/timescale/timescaledb/assets/42375159/6914e802-3fb7-4eb6-ad95-fcd1e0fcfaee)


### TimescaleDB version affected

2.14.2 and 2.15.0

### PostgreSQL version used

15.6

### What operating system did you use?

Arch Linux and Ubuntu Linux

### What installation method did you use?

Deb/Apt

### What platform did you run on?

On prem/Self-hosted

### Relevant log output and stack trace

```bash
May 09 11:40:20 ahmad-82nd postgres[245522]: 2024-05-09 11:40:20.361 +04 [245522] DEBUG:  launching job 1000 "Refresh Continuous Aggregate Policy [1000]"
May 09 11:40:20 ahmad-82nd postgres[246102]: 2024-05-09 11:40:20.368 +04 [246102] DEBUG:  extension state changed: unknown to created
May 09 11:40:20 ahmad-82nd postgres[246102]: 2024-05-09 11:40:20.368 +04 [246102] DEBUG:  Executing policy_refresh_continuous_aggregate with parameters {"end_offset": null, "start_offset": "3 mons", "mat_hypertable_id": 3}
May 09 11:40:20 ahmad-82nd postgres[246102]: 2024-05-09 11:40:20.369 +04 [246102] DEBUG:  rehashing catalog cache id 35 for pg_namespace; 9 tups, 4 buckets
May 09 11:40:20 ahmad-82nd postgres[246102]: 2024-05-09 11:40:20.370 +04 [246102] LOG:  job 1000 threw an error
May 09 11:40:20 ahmad-82nd postgres[246102]: 2024-05-09 11:40:20.370 +04 [246102] ERROR:  timestamp out of range
May 09 11:40:20 ahmad-82nd postgres[180293]: 2024-05-09 11:40:20.375 +04 [180293] LOG:  background worker "Refresh Continuous Aggregate Policy [1000]" (PID 246102) exited with exit code 1
```


### How can we reproduce the bug?

```bash
CREATE TABLE metrics (
    sensor_id INTEGER NOT NULL,
    value DOUBLE PRECISION NOT NULL,
    timestamp TIMESTAMPTZ NOT NULL
);
SELECT create_hypertable('metrics', 'timestamp');


-- insert data into the hyper table
INSERT INTO metrics
SELECT
    s.sensor_id,
    random()*50 + 10,
    timestamp
FROM
    generate_series(DATE (now() - interval '6 month'), (now() - interval '5 day'), INTERVAL '1 hours') AS timestamp
        CROSS JOIN (SELECT generate_series(1, 200) as sensor_id) as s;


-- hourly continuous aggregate with timezone
CREATE MATERIALIZED VIEW datalake_hourly
WITH (timescaledb.continuous, timescaledb.materialized_only = true) AS
            SELECT
                time_bucket(INTERVAL '1 hour', metrics.timestamp, 'Asia/Dubai') AS bucket,
                sensor_id,
                count(*) as count_items
            FROM metrics
            GROUP BY bucket, sensor_id;
           


SELECT add_continuous_aggregate_policy(
        'datalake_hourly',
        start_offset => INTERVAL '3 months',
        end_offset => Null,
        schedule_interval => INTERVAL '15 seconds'
);        


-- insert few more rows
INSERT INTO metrics
SELECT
    s.sensor_id,
    random()*50 + 10,
    timestamp
FROM
    generate_series((now() - interval '5 day'), now(), INTERVAL '1 hour') AS timestamp
        CROSS JOIN (SELECT generate_series(1, 200) as sensor_id) as s;
        
-- wait and check the continuous aggregate
SELECT 
		timescaledb_information.jobs.job_id,
		timescaledb_information.jobs.application_name,
		timescaledb_information.jobs.initial_start,
		timescaledb_information.jobs.next_start,
		timescaledb_information.jobs.config,
		timescaledb_information.jobs.schedule_interval,
		timescaledb_information.jobs.max_runtime ,
		timescaledb_information.jobs.max_retries  ,   
		timescaledb_information.jobs.retry_period ,  
		timescaledb_information.jobs.scheduled ,
		timescaledb_information.jobs.fixed_schedule , 
		timescaledb_information.jobs.hypertable_name , 
		timescaledb_information.continuous_aggregates.view_name ,
		last_run_started_at ,
       job_status,
       last_run_status,
       last_successful_finish ,
       total_runs,
       total_failures ,
       total_successes 
FROM timescaledb_information.jobs
left join timescaledb_information.job_stats on timescaledb_information.jobs.job_id  =  timescaledb_information.job_stats.job_id 
left join timescaledb_information.continuous_aggregates on timescaledb_information.continuous_aggregates.materialization_hypertable_name  =  timescaledb_information.jobs.hypertable_name
order by job_id;
```



Discussion:
Thanks for the script, it reproduces for me. The error is related to the NULL value of the `end_offset`, so probably as a workaround you can set it to something far in the future but not null.


@fabriziomello should the end point calculation follow the same logic as `cagg_get_time_min`? Currently the null end point is returned as max time and not +infinity even for variable-bucket caggs, so the calculations in `ts_compute_inscribed_bucketed_refresh_window_variable` fail.
Hi @akuzm thank you for the reply. I believe in the `end_offset` we can only specify an interval and so this seemed to work with me `'-10 years'` and refreshed the data correctly from today post creating the job.


However I believe we should add a warning or enhance the code related to handling cases with NULL as we had lots of issue with it from back in time
Minimal reproducible test case:
```sql
-- Issue #6902
CREATE TABLE issue_6902 (
  ts  TIMESTAMPTZ NOT NULL,
  temperature NUMERIC
) WITH (
  timescaledb.hypertable,
  timescaledb.partition_column='ts',
  timescaledb.chunk_interval='1 day',
  timescaledb.compress='off'
);

INSERT INTO issue_6902
SELECT t, 1 FROM generate_series(now() - interval '3 hours', now(), interval '1 minute') AS t;

CREATE MATERIALIZED VIEW issue_6902_by_hour
WITH (timescaledb.continuous) AS
SELECT
  time_bucket(INTERVAL '1 hour', ts, 'America/Sao_Paulo') AS bucket, -- using timezone
  MAX(temperature),
  MIN(temperature),
  COUNT(*)
FROM issue_6902
GROUP BY 1
WITH NO DATA;

SELECT add_continuous_aggregate_policy (
  'issue_6902_by_hour',
  start_offset => INTERVAL '3 hours',
  end_offset => NULL,
  schedule_interval => INTERVAL '12 hour',
  initial_start => now() + INTERVAL '12 hour'
) AS job_id \gset

-- 181 rows
CALL run_job(:job_id);
SELECT count(*) FROM issue_6902;

-- run again FAIL
CALL run_job(:job_id);
```



Patch:
diff --git a/.unreleased/pr_8559 b/.unreleased/pr_8559
new file mode 100644
index 00000000000..79123483309
--- /dev/null
+++ b/.unreleased/pr_8559
@@ -0,0 +1,2 @@
+Fixes: #8559 Fix `timestamp out of range` using `end_offset=NULL` on CAgg refresh policy
+Thanks: @nofalx for reporting the error when using `end_offset=NULL` on CAgg refresh policy
diff --git a/tsl/src/bgw_policy/continuous_aggregate_api.c b/tsl/src/bgw_policy/continuous_aggregate_api.c
index 2f91514910f..3181c872694 100644
--- a/tsl/src/bgw_policy/continuous_aggregate_api.c
+++ b/tsl/src/bgw_policy/continuous_aggregate_api.c
@@ -137,7 +137,7 @@ policy_refresh_cagg_get_refresh_end(const Dimension *dim, const Jsonb *config, b
 	int64 res = get_time_from_config(dim, config, POL_REFRESH_CONF_KEY_END_OFFSET, end_isnull);
 
 	if (*end_isnull)
-		return ts_time_get_end_or_max(ts_dimension_get_partition_type(dim));
+		return ts_time_get_noend_or_max(ts_dimension_get_partition_type(dim));
 	return res;
 }
 


Test patch:
diff --git a/tsl/test/expected/cagg_policy.out b/tsl/test/expected/cagg_policy.out
index aaa6d5b8621..d85f7f4a24c 100644
--- a/tsl/test/expected/cagg_policy.out
+++ b/tsl/test/expected/cagg_policy.out
@@ -1400,3 +1400,72 @@ SELECT timescaledb_experimental.add_policies('cagg');
  f
 (1 row)
 
+-- Issue #6902
+-- Fix timestamp out of range in a refresh policy when setting `end_offset=>NULL`
+-- for a CAgg with variable sized bucket (i.e: using `time_bucket` with timezone)
+CREATE TABLE issue_6902 (
+  ts  TIMESTAMPTZ NOT NULL,
+  temperature NUMERIC
+) WITH (
+  timescaledb.hypertable,
+  timescaledb.partition_column='ts',
+  timescaledb.chunk_interval='1 day',
+  timescaledb.compress='off'
+);
+INSERT INTO issue_6902
+SELECT t, 1 FROM generate_series(now() - interval '3 hours', now(), interval '1 minute') AS t;
+CREATE MATERIALIZED VIEW issue_6902_by_hour
+WITH (timescaledb.continuous) AS
+SELECT
+  time_bucket(INTERVAL '1 hour', ts, 'America/Sao_Paulo') AS bucket, -- using timezone
+  MAX(temperature),
+  MIN(temperature),
+  COUNT(*)
+FROM issue_6902
+GROUP BY 1
+WITH NO DATA;
+SELECT add_continuous_aggregate_policy (
+  'issue_6902_by_hour',
+  start_offset => INTERVAL '3 hours',
+  end_offset => NULL,
+  schedule_interval => INTERVAL '12 hour',
+  initial_start => now() + INTERVAL '12 hour'
+) AS job_id \gset
+-- 181 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+ count 
+-------
+   181
+(1 row)
+
+-- run again without any change, remain 181 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+ count 
+-------
+   181
+(1 row)
+
+-- change existing data
+UPDATE issue_6902
+SET temperature = temperature + 1;
+-- run again without any change, remain 181 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+ count 
+-------
+   181
+(1 row)
+
+-- insert more data
+INSERT INTO issue_6902
+SELECT t, 1 FROM generate_series(now() - interval '3 hours', now(), interval '1 minute') AS t;
+-- run again without and should have 362 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+ count 
+-------
+   362
+(1 row)
+
diff --git a/tsl/test/sql/cagg_policy.sql b/tsl/test/sql/cagg_policy.sql
index a13f9251276..34a23813e34 100644
--- a/tsl/test/sql/cagg_policy.sql
+++ b/tsl/test/sql/cagg_policy.sql
@@ -676,3 +676,62 @@ AS SELECT time_bucket(1, a), sum(b)
    FROM t GROUP BY time_bucket(1, a);
 
 SELECT timescaledb_experimental.add_policies('cagg');
+
+-- Issue #6902
+-- Fix timestamp out of range in a refresh policy when setting `end_offset=>NULL`
+-- for a CAgg with variable sized bucket (i.e: using `time_bucket` with timezone)
+CREATE TABLE issue_6902 (
+  ts  TIMESTAMPTZ NOT NULL,
+  temperature NUMERIC
+) WITH (
+  timescaledb.hypertable,
+  timescaledb.partition_column='ts',
+  timescaledb.chunk_interval='1 day',
+  timescaledb.compress='off'
+);
+
+INSERT INTO issue_6902
+SELECT t, 1 FROM generate_series(now() - interval '3 hours', now(), interval '1 minute') AS t;
+
+CREATE MATERIALIZED VIEW issue_6902_by_hour
+WITH (timescaledb.continuous) AS
+SELECT
+  time_bucket(INTERVAL '1 hour', ts, 'America/Sao_Paulo') AS bucket, -- using timezone
+  MAX(temperature),
+  MIN(temperature),
+  COUNT(*)
+FROM issue_6902
+GROUP BY 1
+WITH NO DATA;
+
+SELECT add_continuous_aggregate_policy (
+  'issue_6902_by_hour',
+  start_offset => INTERVAL '3 hours',
+  end_offset => NULL,
+  schedule_interval => INTERVAL '12 hour',
+  initial_start => now() + INTERVAL '12 hour'
+) AS job_id \gset
+
+-- 181 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+
+-- run again without any change, remain 181 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+
+-- change existing data
+UPDATE issue_6902
+SET temperature = temperature + 1;
+
+-- run again without any change, remain 181 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+
+-- insert more data
+INSERT INTO issue_6902
+SELECT t, 1 FROM generate_series(now() - interval '3 hours', now(), interval '1 minute') AS t;
+
+-- run again without and should have 362 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
diff --git a/tsl/test/sql/cagg_policy.sql.orig b/tsl/test/sql/cagg_policy.sql.orig
new file mode 100644
index 00000000000..020f84dc11b
--- /dev/null
+++ b/tsl/test/sql/cagg_policy.sql.orig
@@ -0,0 +1,740 @@
+-- This file and its contents are licensed under the Timescale License.
+-- Please see the included NOTICE for copyright information and
+-- LICENSE-TIMESCALE for a copy of the license.
+
+-- test add and remove refresh policy apis
+
+SET ROLE :ROLE_DEFAULT_PERM_USER;
+
+--TEST1 ---
+--basic test with count
+CREATE TABLE int_tab (a integer, b integer, c integer);
+SELECT table_name FROM create_hypertable('int_tab', 'a', chunk_time_interval=> 10);
+
+INSERT INTO int_tab VALUES( 3 , 16 , 20);
+INSERT INTO int_tab VALUES( 1 , 10 , 20);
+INSERT INTO int_tab VALUES( 1 , 11 , 20);
+INSERT INTO int_tab VALUES( 1 , 12 , 20);
+INSERT INTO int_tab VALUES( 1 , 13 , 20);
+INSERT INTO int_tab VALUES( 1 , 14 , 20);
+INSERT INTO int_tab VALUES( 2 , 14 , 20);
+INSERT INTO int_tab VALUES( 2 , 15 , 20);
+INSERT INTO int_tab VALUES( 2 , 16 , 20);
+
+CREATE OR REPLACE FUNCTION integer_now_int_tab() returns int LANGUAGE SQL STABLE as $$ SELECT coalesce(max(a), 0) FROM int_tab $$;
+SELECT set_integer_now_func('int_tab', 'integer_now_int_tab');
+
+CREATE MATERIALIZED VIEW mat_m1( a, countb )
+WITH (timescaledb.continuous, timescaledb.materialized_only=true)
+as
+SELECT a, count(b)
+FROM int_tab
+GROUP BY time_bucket(1, a), a WITH NO DATA;
+
+\c :TEST_DBNAME :ROLE_SUPERUSER
+
+SET timezone TO PST8PDT;
+
+DELETE FROM _timescaledb_config.bgw_job WHERE TRUE;
+
+SET ROLE :ROLE_DEFAULT_PERM_USER;
+SELECT count(*) FROM _timescaledb_config.bgw_job;
+
+\set ON_ERROR_STOP 0
+\set VERBOSITY default
+
+-- Test 1 step policy for integer type buckets
+ALTER materialized view mat_m1 set (timescaledb.compress = true);
+-- No policy is added if one errors out
+SELECT timescaledb_experimental.add_policies('mat_m1', refresh_start_offset => 1, refresh_end_offset => 10, compress_after => 11, drop_after => 20);
+SELECT timescaledb_experimental.show_policies('mat_m1');
+
+-- All policies are added in one step
+SELECT timescaledb_experimental.add_policies('mat_m1', refresh_start_offset => 10, refresh_end_offset => 1, compress_after => 11, drop_after => 20);
+SELECT timescaledb_experimental.show_policies('mat_m1');
+
+--Test coverage: new view for policies on CAggs
+SELECT * FROM timescaledb_experimental.policies ORDER BY relation_name, proc_name;
+
+--Test coverage: new view for policies only shows the policies for  CAggs
+SELECT add_retention_policy('int_tab', 20);
+SELECT * FROM timescaledb_experimental.policies ORDER BY relation_name, proc_name;
+SELECT remove_retention_policy('int_tab');
+
+-- Test for duplicated policies (issue #5492)
+CREATE MATERIALIZED VIEW mat_m2( a, sumb )
+WITH (timescaledb.continuous, timescaledb.materialized_only=true)
+as
+SELECT a, sum(b)
+FROM int_tab
+GROUP BY time_bucket(1, a), a WITH NO DATA;
+
+-- add refresh policy
+SELECT timescaledb_experimental.add_policies('mat_m2', refresh_start_offset => 10, refresh_end_offset => 1);
+SELECT timescaledb_experimental.show_policies('mat_m2');
+-- check for only one refresh policy for each cagg
+SELECT * FROM timescaledb_experimental.policies WHERE proc_name ~ 'refresh' ORDER BY relation_name, proc_name;
+
+SELECT timescaledb_experimental.remove_all_policies('mat_m2');
+DROP MATERIALIZED VIEW mat_m2;
+
+-- Alter policies
+SELECT timescaledb_experimental.alter_policies('mat_m1',  refresh_start_offset => 11, compress_after=>13, drop_after => 25);
+SELECT timescaledb_experimental.show_policies('mat_m1');
+
+-- Remove one or more policy
+SELECT timescaledb_experimental.remove_policies('mat_m1', false, 'policy_refresh_continuous_aggregate', 'policy_compression');
+SELECT timescaledb_experimental.show_policies('mat_m1');
+
+-- Add one policy
+SELECT timescaledb_experimental.add_policies('mat_m1', refresh_start_offset => 10, refresh_end_offset => 1);
+SELECT timescaledb_experimental.show_policies('mat_m1');
+
+-- Remove all policies
+SELECT timescaledb_experimental.remove_policies('mat_m1', false, 'policy_refresh_continuous_aggregate', 'policy_retention');
+SELECT timescaledb_experimental.show_policies('mat_m1');
+
+--Cross policy checks
+--refresh and compression policy overlap
+SELECT timescaledb_experimental.add_policies('mat_m1', refresh_start_offset => 12, refresh_end_offset => 1, compress_after=>11);
+
+--refresh and retention policy overlap
+SELECT timescaledb_experimental.add_policies('mat_m1', refresh_start_offset => 12, refresh_end_offset => 1, drop_after=>11);
+
+--compression and retention policy overlap
+SELECT timescaledb_experimental.add_policies('mat_m1', compress_after => 10, drop_after => 10);
+
+-- Alter non existent policies
+SELECT timescaledb_experimental.alter_policies('mat_m1', refresh_start_offset => 12, compress_after=>11, drop_after => 15);
+
+ALTER materialized view mat_m1 set (timescaledb.compress = false);
+
+SELECT add_continuous_aggregate_policy('int_tab', '1 day'::interval, 10 , '1 h'::interval);
+SELECT add_continuous_aggregate_policy('mat_m1', '1 day'::interval, 10 , '1 h'::interval);
+SELECT add_continuous_aggregate_policy('mat_m1', '1 day'::interval, 10 );
+SELECT add_continuous_aggregate_policy('mat_m1', 10, '1 day'::interval, '1 h'::interval);
+--start_interval < end_interval
+SELECT add_continuous_aggregate_policy('mat_m1', 5, 10, '1h'::interval);
+--refresh window less than two buckets
+SELECT add_continuous_aggregate_policy('mat_m1', 11, 10, '1h'::interval);
+SELECT add_continuous_aggregate_policy('mat_m1', 20, 10, '1h'::interval) as job_id \gset
+
+--adding again should warn/error
+SELECT add_continuous_aggregate_policy('mat_m1', 20, 10, '1h'::interval, if_not_exists=>false);
+SELECT add_continuous_aggregate_policy('mat_m1', 20, 15, '1h'::interval, if_not_exists=>true);
+SELECT add_continuous_aggregate_policy('mat_m1', 20, 10, '1h'::interval, if_not_exists=>true);
+
+-- modify config and try to add, should error out
+SELECT config FROM _timescaledb_config.bgw_job where id = :job_id;
+SELECT hypertable_id as mat_id FROM _timescaledb_config.bgw_job where id = :job_id \gset
+\set VERBOSITY terse
+\set ON_ERROR_STOP 1
+
+\c :TEST_DBNAME :ROLE_SUPERUSER
+
+SET timezone TO PST8PDT;
+
+UPDATE _timescaledb_config.bgw_job
+SET config = jsonb_build_object('mat_hypertable_id', :mat_id)
+WHERE id = :job_id;
+SET ROLE :ROLE_DEFAULT_PERM_USER;
+SELECT config FROM _timescaledb_config.bgw_job where id = :job_id;
+
+\set ON_ERROR_STOP 0
+\set VERBOSITY default
+SELECT add_continuous_aggregate_policy('mat_m1', 20, 10, '1h'::interval, if_not_exists=>true);
+
+SELECT remove_continuous_aggregate_policy('int_tab');
+SELECT remove_continuous_aggregate_policy('mat_m1');
+-- add with NULL offset, readd with NULL offset
+SELECT add_continuous_aggregate_policy('mat_m1', 20, NULL, '1h'::interval, if_not_exists=>true);
+SELECT add_continuous_aggregate_policy('mat_m1', 20, NULL, '1h'::interval, if_not_exists=>true); -- same param values, so we get a NOTICE
+SELECT add_continuous_aggregate_policy('mat_m1', NULL, NULL, '1h'::interval, if_not_exists=>true); -- different values, so we get a WARNING
+SELECT remove_continuous_aggregate_policy('mat_m1');
+SELECT add_continuous_aggregate_policy('mat_m1', NULL, 20, '1h'::interval, if_not_exists=>true);
+SELECT add_continuous_aggregate_policy('mat_m1', NULL, 20, '1h'::interval, if_not_exists=>true);
+SELECT add_continuous_aggregate_policy('mat_m1', NULL, NULL, '1h'::interval, if_not_exists=>true);
+SELECT remove_continuous_aggregate_policy('mat_m1');
+
+--this one will fail
+SELECT remove_continuous_aggregate_policy('mat_m1');
+SELECT remove_continuous_aggregate_policy('mat_m1', if_not_exists=>true);
+
+--now try to add a policy as a different user than the one that created the cagg
+--should fail
+SET ROLE :ROLE_DEFAULT_PERM_USER_2;
+SELECT add_continuous_aggregate_policy('mat_m1', 20, 10, '1h'::interval) as job_id ;
+\set VERBOSITY terse
+\set ON_ERROR_STOP 1
+
+SET ROLE :ROLE_DEFAULT_PERM_USER;
+DROP MATERIALIZED VIEW mat_m1;
+
+--- code coverage tests : add policy for timestamp and date based table ---
+CREATE TABLE continuous_agg_max_mat_date(time DATE);
+SELECT create_hypertable('continuous_agg_max_mat_date', 'time');
+CREATE MATERIALIZED VIEW max_mat_view_date
+	WITH (timescaledb.continuous, timescaledb.materialized_only=true)
+	AS SELECT time_bucket('7 days', time)
+		FROM continuous_agg_max_mat_date
+		GROUP BY 1 WITH NO DATA;
+
+\set ON_ERROR_STOP 0
+\set VERBOSITY default
+
+-- Test 1 step policy for timestamp type buckets
+ALTER materialized view max_mat_view_date set (timescaledb.compress = true);
+-- Only works for cagg
+SELECT timescaledb_experimental.add_policies('continuous_agg_max_mat_date', refresh_start_offset => '1 day'::interval, refresh_end_offset => '2 day'::interval, compress_after => '20 days'::interval, drop_after => '25 days'::interval);
+SELECT timescaledb_experimental.show_policies('continuous_agg_max_mat_date');
+SELECT timescaledb_experimental.alter_policies('continuous_agg_max_mat_date', compress_after=>'16 days'::interval);
+SELECT timescaledb_experimental.remove_policies('continuous_agg_max_mat_date', false, 'policy_refresh_continuous_aggregate');
+
+-- No policy is added if one errors out
+SELECT timescaledb_experimental.add_policies('max_mat_view_date', refresh_start_offset => '1 day'::interval, refresh_end_offset => '2 day'::interval, compress_after => '20 days'::interval, drop_after => '25 days'::interval);
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+-- Create open ended refresh_policy
+SELECT timescaledb_experimental.add_policies('max_mat_view_date', refresh_end_offset => '2 day'::interval);
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+SELECT timescaledb_experimental.remove_policies('max_mat_view_date', false, 'policy_refresh_continuous_aggregate');
+
+SELECT timescaledb_experimental.add_policies('max_mat_view_date', refresh_end_offset => '2 day'::interval, refresh_start_offset=>'-infinity');
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+SELECT timescaledb_experimental.remove_policies('max_mat_view_date', false, 'policy_refresh_continuous_aggregate');
+
+SELECT timescaledb_experimental.add_policies('max_mat_view_date', refresh_start_offset => '2 day'::interval);
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+SELECT timescaledb_experimental.remove_policies('max_mat_view_date', false, 'policy_refresh_continuous_aggregate');
+
+SELECT timescaledb_experimental.add_policies('max_mat_view_date', refresh_start_offset => '2 day'::interval, refresh_end_offset=>'infinity');
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+SELECT timescaledb_experimental.remove_policies('max_mat_view_date', false, 'policy_refresh_continuous_aggregate');
+
+-- Open ended at both sides, for code coverage
+SELECT timescaledb_experimental.add_policies('max_mat_view_date', refresh_end_offset => 'infinity', refresh_start_offset => '-infinity');
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+SELECT timescaledb_experimental.remove_policies('max_mat_view_date', false, 'policy_refresh_continuous_aggregate');
+
+-- All policies are added in one step
+SELECT timescaledb_experimental.add_policies('max_mat_view_date', refresh_start_offset => '15 days'::interval, refresh_end_offset => '1 day'::interval, compress_after => '20 days'::interval, drop_after => '25 days'::interval);
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+-- Alter policies
+SELECT timescaledb_experimental.alter_policies('max_mat_view_date', refresh_start_offset => '16 days'::interval, compress_after=>'26 days'::interval, drop_after => '40 days'::interval);
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+--Alter refresh_policy to make it open ended
+SELECT timescaledb_experimental.remove_policies('max_mat_view_date', false, 'policy_retention', 'policy_compression');
+SELECT timescaledb_experimental.alter_policies('max_mat_view_date', refresh_start_offset =>'-infinity');
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+SELECT timescaledb_experimental.alter_policies('max_mat_view_date', refresh_end_offset =>'infinity', refresh_start_offset =>'5 days'::interval);
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+--Cross policy checks
+-- Refresh and compression policies overlap
+SELECT timescaledb_experimental.add_policies('max_mat_view_date', compress_after => '20 days'::interval, drop_after => '25 days'::interval);
+SELECT timescaledb_experimental.alter_policies('max_mat_view_date', compress_after=> '4 days'::interval);
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+-- Refresh and retention policies overlap
+SELECT timescaledb_experimental.alter_policies('max_mat_view_date', refresh_start_offset =>'5 days'::interval, drop_after=> '4 days'::interval);
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+--Do not allow refreshed data to be deleted
+SELECT add_retention_policy('continuous_agg_max_mat_date', '25 days'::interval);
+SELECT timescaledb_experimental.alter_policies('max_mat_view_date', refresh_start_offset =>'25 days'::interval);
+SELECT remove_retention_policy('continuous_agg_max_mat_date');
+
+-- Remove one or more policy
+-- Code coverage: no policy names provided
+SELECT timescaledb_experimental.remove_policies('max_mat_view_date', false);
+
+-- Code coverage: incorrect name of policy
+SELECT timescaledb_experimental.remove_policies('max_mat_view_date', false, 'refresh_policy');
+
+SELECT timescaledb_experimental.remove_policies('max_mat_view_date', false, 'policy_refresh_continuous_aggregate', 'policy_compression');
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+-- Add one policy
+SELECT timescaledb_experimental.add_policies('max_mat_view_date', refresh_start_offset => '15 day'::interval, refresh_end_offset => '1 day'::interval);
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+-- Remove all policies
+SELECT * FROM timescaledb_experimental.policies ORDER BY relation_name, proc_name;
+SELECT timescaledb_experimental.remove_all_policies(NULL); -- should fail
+SELECT timescaledb_experimental.remove_all_policies('continuous_agg_max_mat_date'); -- should fail
+SELECT timescaledb_experimental.remove_all_policies('max_mat_view_date', false);
+SELECT timescaledb_experimental.remove_all_policies('max_mat_view_date', false); -- should fail
+CREATE OR REPLACE FUNCTION custom_func(jobid int, args jsonb) RETURNS RECORD LANGUAGE SQL AS
+$$
+  VALUES($1, $2, 'custom_func');
+$$;
+ -- inject custom job
+SELECT add_job('custom_func','1h', config:='{"type":"function"}'::jsonb, initial_start => '2000-01-01 00:00:00+00'::timestamptz) AS job_id \gset
+SELECT _timescaledb_functions.alter_job_set_hypertable_id( :job_id, 'max_mat_view_date'::regclass);
+SELECT * FROM timescaledb_information.jobs WHERE job_id != 1 ORDER BY 1;
+SELECT timescaledb_experimental.remove_all_policies('max_mat_view_date', true); -- ignore custom job
+SELECT delete_job(:job_id);
+DROP FUNCTION custom_func;
+SELECT timescaledb_experimental.show_policies('max_mat_view_date');
+
+ALTER materialized view max_mat_view_date set (timescaledb.compress = false);
+
+SELECT add_continuous_aggregate_policy('max_mat_view_date', '2 days', 10, '1 day'::interval);
+--start_interval < end_interval
+SELECT add_continuous_aggregate_policy('max_mat_view_date', '1 day'::interval, '2 days'::interval , '1 day'::interval) ;
+--interval less than two buckets
+SELECT add_continuous_aggregate_policy('max_mat_view_date', '7 days', '1 day', '1 day'::interval);
+SELECT add_continuous_aggregate_policy('max_mat_view_date', '14 days', '1 day', '1 day'::interval);
+SELECT add_continuous_aggregate_policy('max_mat_view_date', '13 days', '-10 hours', '1 day'::interval);
+\set VERBOSITY terse
+\set ON_ERROR_STOP 1
+
+-- Negative start offset gives two bucket window:
+SELECT add_continuous_aggregate_policy('max_mat_view_date', '13 days', '-1 day', '1 day'::interval);
+SELECT remove_continuous_aggregate_policy('max_mat_view_date');
+-- Both offsets NULL:
+SELECT add_continuous_aggregate_policy('max_mat_view_date', NULL, NULL, '1 day'::interval);
+SELECT remove_continuous_aggregate_policy('max_mat_view_date');
+
+SELECT add_continuous_aggregate_policy('max_mat_view_date', '15 days', '1 day', '1 day'::interval) as job_id \gset
+SELECT config FROM _timescaledb_config.bgw_job
+WHERE id = :job_id;
+
+INSERT INTO continuous_agg_max_mat_date
+	SELECT generate_series('2019-09-01'::date, '2019-09-10'::date, '1 day');
+--- to prevent NOTICES set message level to warning
+SET client_min_messages TO warning;
+CALL run_job(:job_id);
+RESET client_min_messages;
+DROP MATERIALIZED VIEW max_mat_view_date;
+
+CREATE TABLE continuous_agg_timestamp(time TIMESTAMP);
+SELECT create_hypertable('continuous_agg_timestamp', 'time');
+
+CREATE MATERIALIZED VIEW max_mat_view_timestamp
+	WITH (timescaledb.continuous, timescaledb.materialized_only=true)
+	AS SELECT time_bucket('7 days', time)
+		FROM continuous_agg_timestamp
+		GROUP BY 1 WITH NO DATA;
+
+--the start offset overflows the smallest time value, but is capped at
+--the min value
+SELECT add_continuous_aggregate_policy('max_mat_view_timestamp', '1000000 years', '1 day' , '1 h'::interval);
+SELECT remove_continuous_aggregate_policy('max_mat_view_timestamp');
+
+\set ON_ERROR_STOP 0
+\set VERBOSITY default
+--start and end offset capped at the lowest time value, which means
+--zero size window
+SELECT add_continuous_aggregate_policy('max_mat_view_timestamp', '1000000 years', '900000 years' , '1 h'::interval);
+SELECT add_continuous_aggregate_policy('max_mat_view_timestamp', '301 days', '10 months' , '1 h'::interval);
+\set VERBOSITY terse
+\set ON_ERROR_STOP 1
+
+SELECT add_continuous_aggregate_policy('max_mat_view_timestamp', '15 days', '1 h'::interval , '1 h'::interval) as job_id \gset
+
+--- to prevent NOTICES set message level to warning
+SET client_min_messages TO warning;
+CALL run_job(:job_id);
+RESET client_min_messages ;
+
+SELECT config FROM _timescaledb_config.bgw_job
+WHERE id = :job_id;
+
+\c :TEST_DBNAME :ROLE_SUPERUSER
+
+SET timezone TO PST8PDT;
+
+UPDATE _timescaledb_config.bgw_job
+SET config = jsonb_build_object('mat_hypertable_id', :mat_id)
+WHERE id = :job_id;
+
+SET ROLE :ROLE_DEFAULT_PERM_USER;
+SELECT config FROM _timescaledb_config.bgw_job where id = :job_id;
+\set ON_ERROR_STOP 0
+SELECT add_continuous_aggregate_policy('max_mat_view_timestamp', '15 day', '1 day', '1h'::interval, if_not_exists=>true);
+SELECT add_continuous_aggregate_policy('max_mat_view_timestamp', 'xyz', '1 day', '1h'::interval, if_not_exists=>true);
+\set ON_ERROR_STOP 1
+
+DROP MATERIALIZED VIEW max_mat_view_timestamp;
+
+--smallint table
+CREATE TABLE smallint_tab (a smallint);
+SELECT table_name FROM create_hypertable('smallint_tab', 'a', chunk_time_interval=> 10);
+CREATE OR REPLACE FUNCTION integer_now_smallint_tab() returns smallint LANGUAGE SQL STABLE as $$ SELECT coalesce(max(a)::smallint, 0::smallint) FROM smallint_tab ; $$;
+SELECT set_integer_now_func('smallint_tab', 'integer_now_smallint_tab');
+
+CREATE MATERIALIZED VIEW mat_smallint( a, countb )
+WITH (timescaledb.continuous, timescaledb.materialized_only=true)
+as
+SELECT time_bucket( SMALLINT '1', a) , count(*)
+FROM smallint_tab
+GROUP BY 1 WITH NO DATA;
+\set ON_ERROR_STOP 0
+\set VERBOSITY default
+
+-- Test 1 step policy for smallint type buckets
+ALTER materialized view mat_smallint set (timescaledb.compress = true);
+
+-- All policies are added in one step
+SELECT timescaledb_experimental.add_policies('mat_smallint', refresh_start_offset => 10::smallint, refresh_end_offset => 1::smallint, compress_after => 11::smallint, drop_after => 20::smallint);
+SELECT timescaledb_experimental.show_policies('mat_smallint');
+
+-- Alter policies
+SELECT timescaledb_experimental.alter_policies('mat_smallint',  refresh_start_offset => 11::smallint, compress_after=>13::smallint, drop_after => 25::smallint);
+SELECT timescaledb_experimental.show_policies('mat_smallint');
+
+SELECT timescaledb_experimental.remove_all_policies('mat_smallint', false);
+ALTER materialized view mat_smallint set (timescaledb.compress = false);
+
+SELECT add_continuous_aggregate_policy('mat_smallint', 15, 0 , '1 h'::interval);
+SELECT add_continuous_aggregate_policy('mat_smallint', 98898::smallint , 0::smallint, '1 h'::interval);
+SELECT add_continuous_aggregate_policy('mat_smallint', 5::smallint, 10::smallint , '1 h'::interval) as job_id \gset
+\set VERBOSITY terse
+\set ON_ERROR_STOP 1
+SELECT add_continuous_aggregate_policy('mat_smallint', 15::smallint, 0::smallint , '1 h'::interval) as job_id \gset
+INSERT INTO smallint_tab VALUES(5);
+INSERT INTO smallint_tab VALUES(10);
+INSERT INTO smallint_tab VALUES(20);
+CALL run_job(:job_id);
+SELECT * FROM mat_smallint ORDER BY 1;
+
+--remove all the data--
+TRUNCATE table smallint_tab;
+CALL refresh_continuous_aggregate('mat_smallint', NULL, NULL);
+SELECT * FROM mat_smallint ORDER BY 1;
+
+-- Case 1: overflow by subtracting from PG_INT16_MIN
+--overflow start_interval, end_interval [-32768, -32768)
+SELECT remove_continuous_aggregate_policy('mat_smallint');
+INSERT INTO smallint_tab VALUES( -32768 );
+SELECT integer_now_smallint_tab();
+SELECT add_continuous_aggregate_policy('mat_smallint', 10::smallint, 5::smallint , '1 h'::interval) as job_id \gset
+
+\set ON_ERROR_STOP 0
+CALL run_job(:job_id);
+\set ON_ERROR_STOP 1
+SELECT * FROM mat_smallint ORDER BY 1;
+
+-- overflow start_interval. now this runs as range is capped [-32768, -32765)
+INSERT INTO smallint_tab VALUES( -32760 );
+SELECT maxval, maxval - 10, maxval -5 FROM integer_now_smallint_tab() as maxval;
+CALL run_job(:job_id);
+SELECT * FROM mat_smallint ORDER BY 1;
+
+--remove all the data--
+TRUNCATE table smallint_tab;
+CALL refresh_continuous_aggregate('mat_smallint', NULL, NULL);
+SELECT * FROM mat_smallint ORDER BY 1;
+
+-- Case 2: overflow by subtracting from PG_INT16_MAX
+--overflow start and end . will fail as range is [32767, 32767]
+SELECT remove_continuous_aggregate_policy('mat_smallint');
+INSERT INTO smallint_tab VALUES( 32766 );
+INSERT INTO smallint_tab VALUES( 32767 );
+SELECT maxval, maxval - (-1), maxval - (-2) FROM integer_now_smallint_tab() as maxval;
+SELECT add_continuous_aggregate_policy('mat_smallint', -1::smallint, -3::smallint , '1 h'::interval) as job_id \gset
+\set ON_ERROR_STOP 0
+CALL run_job(:job_id);
+\set ON_ERROR_STOP 1
+SELECT * FROM mat_smallint ORDER BY 1;
+
+SELECT remove_continuous_aggregate_policy('mat_smallint');
+--overflow end . will work range is [32765, 32767)
+SELECT maxval, maxval - (1), maxval - (-2) FROM integer_now_smallint_tab() as maxval;
+SELECT add_continuous_aggregate_policy('mat_smallint', 1::smallint, -3::smallint , '1 h'::interval) as job_id \gset
+\set ON_ERROR_STOP 0
+CALL run_job(:job_id);
+SELECT * FROM mat_smallint ORDER BY 1;
+
+-- tests for interval argument conversions
+--
+\set ON_ERROR_STOP 0
+SELECT add_continuous_aggregate_policy('mat_smallint', 15, 10, '1h'::interval, if_not_exists=>true);
+SELECT add_continuous_aggregate_policy('mat_smallint', '15', 10, '1h'::interval, if_not_exists=>true);
+SELECT add_continuous_aggregate_policy('mat_smallint', '15', '10', '1h'::interval, if_not_exists=>true);
+\set ON_ERROR_STOP 1
+
+
+--bigint table
+CREATE TABLE bigint_tab (a bigint);
+SELECT table_name FROM create_hypertable('bigint_tab', 'a', chunk_time_interval=> 10);
+CREATE OR REPLACE FUNCTION integer_now_bigint_tab() returns bigint LANGUAGE SQL STABLE as $$ SELECT 20::bigint $$;
+SELECT set_integer_now_func('bigint_tab', 'integer_now_bigint_tab');
+
+CREATE MATERIALIZED VIEW mat_bigint( a, countb )
+WITH (timescaledb.continuous, timescaledb.materialized_only=true)
+as
+SELECT time_bucket( BIGINT '1', a) , count(*)
+FROM bigint_tab
+GROUP BY 1 WITH NO DATA;
+
+-- Test 1 step policy for bigint type buckets
+ALTER materialized view mat_bigint set (timescaledb.compress = true);
+
+-- All policies are added in one step
+SELECT timescaledb_experimental.add_policies('mat_bigint', refresh_start_offset => 10::bigint, refresh_end_offset => 1::bigint, compress_after => 11::bigint, drop_after => 20::bigint);
+SELECT timescaledb_experimental.show_policies('mat_bigint');
+
+-- Alter policies
+SELECT timescaledb_experimental.alter_policies('mat_bigint',  refresh_start_offset => 11::bigint, compress_after=>13::bigint, drop_after => 25::bigint);
+SELECT timescaledb_experimental.show_policies('mat_bigint');
+
+SELECT timescaledb_experimental.remove_all_policies('mat_bigint', false);
+ALTER materialized view mat_bigint set (timescaledb.compress = false);
+
+\set ON_ERROR_STOP 0
+SELECT add_continuous_aggregate_policy('mat_bigint', 5::bigint, 10::bigint , '1 h'::interval) ;
+\set ON_ERROR_STOP 1
+SELECT add_continuous_aggregate_policy('mat_bigint', 15::bigint, 0::bigint , '1 h'::interval) as job_mid \gset
+INSERT INTO bigint_tab VALUES(5);
+INSERT INTO bigint_tab VALUES(10);
+INSERT INTO bigint_tab VALUES(20);
+CALL run_job(:job_mid);
+SELECT * FROM mat_bigint ORDER BY 1;
+
+-- test NULL for end
+SELECT remove_continuous_aggregate_policy('mat_bigint');
+SELECT add_continuous_aggregate_policy('mat_bigint', 1::smallint, NULL , '1 h'::interval) as job_id \gset
+INSERT INTO bigint_tab VALUES(500);
+CALL run_job(:job_id);
+SELECT * FROM mat_bigint WHERE a>100 ORDER BY 1;
+
+ALTER MATERIALIZED VIEW mat_bigint SET (timescaledb.compress);
+ALTER MATERIALIZED VIEW mat_smallint SET (timescaledb.compress);
+-- With immutable compressed chunks, these policies would fail by overlapping the refresh window
+SELECT add_compression_policy('mat_smallint', -4::smallint);
+SELECT remove_compression_policy('mat_smallint');
+SELECT add_compression_policy('mat_bigint', 0::bigint);
+SELECT remove_compression_policy('mat_bigint');
+-- End previous limitation tests
+
+SELECT add_compression_policy('mat_smallint', 5::smallint);
+SELECT add_compression_policy('mat_bigint', 20::bigint);
+
+-- end of coverage tests
+
+--TEST continuous aggregate + compression policy on caggs
+CREATE TABLE metrics (
+    time timestamptz NOT NULL,
+    device_id int,
+    device_id_peer int,
+    v0 int,
+    v1 int,
+    v2 float,
+    v3 float
+);
+
+SELECT create_hypertable('metrics', 'time');
+
+INSERT INTO metrics (time, device_id, device_id_peer, v0, v1, v2, v3)
+SELECT time,
+    device_id,
+    0,
+    device_id + 1,
+    device_id + 2,
+    0.5,
+    NULL
+FROM generate_series('2000-01-01 0:00:00+0'::timestamptz, '2000-01-02 23:55:00+0', '20m') gtime (time),
+    generate_series(1, 2, 1) gdevice (device_id);
+
+ALTER TABLE metrics SET ( timescaledb.compress );
+SELECT compress_chunk(ch) FROM show_chunks('metrics') ch;
+
+CREATE MATERIALIZED VIEW metrics_cagg WITH (timescaledb.continuous,
+  timescaledb.materialized_only = true)
+AS
+SELECT time_bucket('1 day', time) as dayb, device_id,
+       sum(v0), avg(v3)
+FROM metrics
+GROUP BY 1, 2
+WITH NO DATA;
+
+-- this was previously crashing
+SELECT add_continuous_aggregate_policy('metrics_cagg', '7 day'::interval, NULL, '1 h'::interval, if_not_exists => true);
+\set ON_ERROR_STOP 0
+SELECT add_continuous_aggregate_policy('metrics_cagg', '7 day'::interval, '1 day'::interval, '1 h'::interval, if_not_exists => true);
+SELECT remove_continuous_aggregate_policy('metrics_cagg');
+SELECT add_continuous_aggregate_policy('metrics_cagg', NULL, '1 day'::interval, '1h'::interval, if_not_exists=>true);
+SELECT add_continuous_aggregate_policy('metrics_cagg', NULL, '1 day'::interval, '1h'::interval, if_not_exists=>true); -- same param values, so we get a NOTICE
+SELECT add_continuous_aggregate_policy('metrics_cagg', NULL, NULL, '1h'::interval, if_not_exists=>true); -- different values, so we get a WARNING
+SELECT remove_continuous_aggregate_policy('metrics_cagg');
+--can set compression policy only after setting up refresh policy --
+SELECT add_compression_policy('metrics_cagg', '1 day'::interval);
+
+--can set compression policy only after enabling compression --
+SELECT add_continuous_aggregate_policy('metrics_cagg', '7 day'::interval, '1 day'::interval, '1 h'::interval) as "REFRESH_JOB" \gset
+SELECT add_compression_policy('metrics_cagg', '8 day'::interval) AS "COMP_JOB" ;
+ALTER MATERIALIZED VIEW metrics_cagg SET (timescaledb.compress);
+
+--cannot use compress_created_before with cagg
+SELECT add_compression_policy('metrics_cagg', compress_created_before => '8 day'::interval) AS "COMP_JOB" ;
+\set ON_ERROR_STOP 1
+
+
+SELECT add_compression_policy('metrics_cagg', '8 day'::interval) AS "COMP_JOB" ;
+SELECT remove_compression_policy('metrics_cagg');
+SELECT add_compression_policy('metrics_cagg', '8 day'::interval) AS "COMP_JOB" \gset
+
+--verify that jobs were added for the policies ---
+SELECT materialization_hypertable_name AS "MAT_TABLE_NAME",
+       view_name AS "VIEW_NAME"
+FROM timescaledb_information.continuous_aggregates
+WHERE view_name = 'metrics_cagg' \gset
+
+SELECT count(*) FROM timescaledb_information.jobs
+WHERE hypertable_name = :'VIEW_NAME';
+
+--exec the cagg compression job --
+CALL refresh_continuous_aggregate('metrics_cagg', NULL, '2001-02-01 00:00:00+0');
+CALL run_job(:COMP_JOB);
+SELECT count(*), count(*) FILTER ( WHERE is_compressed is TRUE  )
+FROM timescaledb_information.chunks
+WHERE hypertable_name = :'MAT_TABLE_NAME' ORDER BY 1;
+
+--add some new data into metrics_cagg so that cagg policy job has something to do
+INSERT INTO metrics (time, device_id, device_id_peer, v0, v1, v2, v3)
+SELECT now() - '5 day'::interval, 102, 0, 10, 10, 10, 10;
+CALL run_job(:REFRESH_JOB);
+--now we have a new chunk and it is not compressed
+SELECT count(*), count(*) FILTER ( WHERE is_compressed is TRUE  )
+FROM timescaledb_information.chunks
+WHERE hypertable_name = :'MAT_TABLE_NAME' ORDER BY 1;
+
+--verify that both jobs are dropped when view is dropped
+DROP MATERIALIZED VIEW metrics_cagg;
+
+SELECT count(*) FROM timescaledb_information.jobs
+WHERE hypertable_name = :'VIEW_NAME';
+
+-- add test case for issue 4252
+CREATE TABLE IF NOT EXISTS sensor_data(
+time TIMESTAMPTZ NOT NULL,
+sensor_id INTEGER,
+temperature DOUBLE PRECISION,
+cpu DOUBLE PRECISION);
+
+SELECT create_hypertable('sensor_data','time');
+
+INSERT INTO sensor_data(time, sensor_id, cpu, temperature)
+SELECT
+time,
+sensor_id,
+extract(dow from time) AS cpu,
+extract(doy from time) AS temperature
+FROM
+generate_series('2022-05-05'::timestamp at time zone 'UTC' - interval '6 weeks', '2022-05-05'::timestamp at time zone 'UTC', interval '5 hours') as g1(time),
+generate_series(1,1000,1) as g2(sensor_id);
+
+CREATE materialized view deals_best_weekly
+WITH (timescaledb.continuous) AS
+SELECT
+time_bucket('7 days', "time") AS bucket,
+avg(temperature) AS avg_temp,
+max(cpu) AS max_rating
+FROM sensor_data
+GROUP BY bucket
+WITH NO DATA;
+
+CREATE materialized view deals_best_daily
+WITH (timescaledb.continuous) AS
+SELECT
+time_bucket('1 day', "time") AS bucket,
+avg(temperature) AS avg_temp,
+max(cpu) AS max_rating
+FROM sensor_data
+GROUP BY bucket
+WITH NO DATA;
+
+ALTER materialized view deals_best_weekly set (timescaledb.materialized_only=true);
+ALTER materialized view deals_best_daily set (timescaledb.materialized_only=true);
+
+-- we have data from 6 weeks before to May 5 2022 (Thu)
+CALL refresh_continuous_aggregate('deals_best_weekly', '2022-04-24', '2022-05-03');
+SELECT * FROM deals_best_weekly ORDER BY bucket;
+CALL refresh_continuous_aggregate('deals_best_daily', '2022-04-20', '2022-05-04');
+SELECT * FROM deals_best_daily ORDER BY bucket LIMIT 2;
+-- expect to get an up-to-date notice
+CALL refresh_continuous_aggregate('deals_best_weekly', '2022-04-24', '2022-05-05');
+SELECT * FROM deals_best_weekly ORDER BY bucket;
+
+-- github issue 5907: segfault when creating 1-step policies on cagg
+-- whose underlying hypertable has a retention policy setup
+CREATE TABLE t(a integer NOT NULL, b integer);
+SELECT create_hypertable('t', 'a', chunk_time_interval=> 10);
+
+CREATE OR REPLACE FUNCTION unix_now() returns int LANGUAGE SQL IMMUTABLE as $$ SELECT extract(epoch from now())::INT $$;
+SELECT set_integer_now_func('t', 'unix_now');
+
+SELECT add_retention_policy('t', 20);
+
+CREATE MATERIALIZED VIEW cagg(a, sumb) WITH (timescaledb.continuous)
+AS SELECT time_bucket(1, a), sum(b)
+   FROM t GROUP BY time_bucket(1, a);
+
+SELECT timescaledb_experimental.add_policies('cagg');
+
+-- Issue #6902
+<<<<<<< HEAD
+-- Fix timestamp out of range in a refresh policy when setting `end_offset=>NULL` 
+-- for a CAgg with variable sized bucket (i.e: using `time_bucket` with timezone)
+=======
+>>>>>>> cda33125c (Fix timestamp out of range in CAgg refresh policy)
+CREATE TABLE issue_6902 (
+  ts  TIMESTAMPTZ NOT NULL,
+  temperature NUMERIC
+) WITH (
+  timescaledb.hypertable,
+  timescaledb.partition_column='ts',
+  timescaledb.chunk_interval='1 day',
+  timescaledb.compress='off'
+);
+
+INSERT INTO issue_6902
+SELECT t, 1 FROM generate_series(now() - interval '3 hours', now(), interval '1 minute') AS t;
+
+CREATE MATERIALIZED VIEW issue_6902_by_hour
+WITH (timescaledb.continuous) AS
+SELECT
+  time_bucket(INTERVAL '1 hour', ts, 'America/Sao_Paulo') AS bucket, -- using timezone
+  MAX(temperature),
+  MIN(temperature),
+  COUNT(*)
+FROM issue_6902
+GROUP BY 1
+WITH NO DATA;
+
+SELECT add_continuous_aggregate_policy (
+  'issue_6902_by_hour',
+  start_offset => INTERVAL '3 hours',
+  end_offset => NULL,
+  schedule_interval => INTERVAL '12 hour',
+  initial_start => now() + INTERVAL '12 hour'
+) AS job_id \gset
+
+-- 181 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+
+-- run again without any change, remain 181 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+
+-- change existing data
+UPDATE issue_6902
+SET temperature = temperature + 1;
+
+-- run again without any change, remain 181 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;
+
+-- insert more data
+INSERT INTO issue_6902
+SELECT t, 1 FROM generate_series(now() - interval '3 hours', now(), interval '1 minute') AS t;
+
+-- run again without and should have 362 rows
+CALL run_job(:job_id);
+SELECT count(*) FROM issue_6902;