-- 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.
CREATE TABLE custom_log(job_id int, args jsonb, extra text, runner NAME DEFAULT CURRENT_ROLE);
CREATE OR REPLACE FUNCTION custom_func(jobid int, args jsonb) RETURNS VOID LANGUAGE SQL AS
$$
  INSERT INTO custom_log VALUES($1, $2, 'custom_func');
$$;
CREATE OR REPLACE FUNCTION custom_func_definer(jobid int, args jsonb) RETURNS VOID LANGUAGE SQL AS
$$
  INSERT INTO custom_log VALUES($1, $2, 'security definer');
$$ SECURITY DEFINER;
CREATE OR REPLACE PROCEDURE custom_proc(job_id int, args jsonb) LANGUAGE SQL AS
$$
  INSERT INTO custom_log VALUES($1, $2, 'custom_proc');
$$;
-- procedure with transaction handling
CREATE OR REPLACE PROCEDURE custom_proc2(job_id int, args jsonb) LANGUAGE PLPGSQL AS
$$
BEGIN
  INSERT INTO custom_log VALUES($1, $2, 'custom_proc2 1 COMMIT ' || (args->>'type'));
  COMMIT;
  INSERT INTO custom_log VALUES($1, $2, 'custom_proc2 2 ROLLBACK ' || (args->>'type'));
  ROLLBACK;
  INSERT INTO custom_log VALUES($1, $2, 'custom_proc2 3 COMMIT ' || (args->>'type'));
  COMMIT;
END
$$;
\set ON_ERROR_STOP 0
-- test bad input
SELECT add_job(NULL, '1h');
ERROR:  function or procedure cannot be NULL
SELECT add_job(0, '1h');
ERROR:  function or procedure with OID 0 does not exist
-- this will return an error about Oid 4294967295
-- while regproc is unsigned int postgres has an implicit cast from int to regproc
SELECT add_job(-1, '1h');
ERROR:  function or procedure with OID 4294967295 does not exist
SELECT add_job('invalid_func', '1h');
ERROR:  function "invalid_func" does not exist at character 16
SELECT add_job('custom_func', NULL);
ERROR:  schedule interval cannot be NULL
SELECT add_job('custom_func', 'invalid interval');
ERROR:  invalid input syntax for type interval: "invalid interval" at character 31
SELECT add_job('custom_func', '1h', job_name := 'this_is_a_really_really_really_long_application_name_to_overflow');
ERROR:  application name too long.
\set ON_ERROR_STOP 1
select '2000-01-01 00:00:00+00' as time_zero \gset
SELECT add_job('custom_func','1h', config:='{"type":"function"}'::jsonb, initial_start => :'time_zero'::TIMESTAMPTZ);
 add_job 
---------
    1001

SELECT add_job('custom_proc','1h', config:='{"type":"procedure"}'::jsonb, initial_start => :'time_zero'::TIMESTAMPTZ);
 add_job 
---------
    1002

SELECT add_job('custom_proc2','1h', config:= '{"type":"procedure"}'::jsonb, initial_start => :'time_zero'::TIMESTAMPTZ);
 add_job 
---------
    1003

SELECT add_job('custom_func', '1h', config:='{"type":"function"}'::jsonb, initial_start => :'time_zero'::TIMESTAMPTZ);
 add_job 
---------
    1004

SELECT add_job('custom_func_definer', '1h', config:='{"type":"function"}'::jsonb, initial_start => :'time_zero'::TIMESTAMPTZ, job_name := 'custom_job_name');
 add_job 
---------
    1005

-- exclude internal jobs
SELECT * FROM timescaledb_information.jobs WHERE job_id >= 1000 ORDER BY 1;
 job_id |      application_name      | schedule_interval | max_runtime | max_retries | retry_period | proc_schema |      proc_name      |       owner       | scheduled | fixed_schedule |        config         |          next_start          |        initial_start         | hypertable_schema | hypertable_name | check_schema | check_name 
--------+----------------------------+-------------------+-------------+-------------+--------------+-------------+---------------------+-------------------+-----------+----------------+-----------------------+------------------------------+------------------------------+-------------------+-----------------+--------------+------------
   1001 | User-Defined Action [1001] | @ 1 hour          | @ 0         |          -1 | @ 5 mins     | public      | custom_func         | default_perm_user | t         | t              | {"type": "function"}  | Fri Dec 31 16:00:00 1999 PST | Fri Dec 31 16:00:00 1999 PST |                   |                 |              | 
   1002 | User-Defined Action [1002] | @ 1 hour          | @ 0         |          -1 | @ 5 mins     | public      | custom_proc         | default_perm_user | t         | t              | {"type": "procedure"} | Fri Dec 31 16:00:00 1999 PST | Fri Dec 31 16:00:00 1999 PST |                   |                 |              | 
   1003 | User-Defined Action [1003] | @ 1 hour          | @ 0         |          -1 | @ 5 mins     | public      | custom_proc2        | default_perm_user | t         | t              | {"type": "procedure"} | Fri Dec 31 16:00:00 1999 PST | Fri Dec 31 16:00:00 1999 PST |                   |                 |              | 
   1004 | User-Defined Action [1004] | @ 1 hour          | @ 0         |          -1 | @ 5 mins     | public      | custom_func         | default_perm_user | t         | t              | {"type": "function"}  | Fri Dec 31 16:00:00 1999 PST | Fri Dec 31 16:00:00 1999 PST |                   |                 |              | 
   1005 | custom_job_name [1005]     | @ 1 hour          | @ 0         |          -1 | @ 5 mins     | public      | custom_func_definer | default_perm_user | t         | t              | {"type": "function"}  | Fri Dec 31 16:00:00 1999 PST | Fri Dec 31 16:00:00 1999 PST |                   |                 |              | 

SELECT count(*) FROM _timescaledb_catalog.bgw_job WHERE config->>'type' IN ('procedure', 'function');
 count 
-------
     5

\set ON_ERROR_STOP 0
-- test bad input
CALL run_job(NULL);
ERROR:  job ID cannot be NULL
CALL run_job(-1);
ERROR:  job -1 not found
\set ON_ERROR_STOP 1
CALL run_job(1001);
CALL run_job(1002);
CALL run_job(1003);
CALL run_job(1004);
CALL run_job(1005);
SELECT * FROM custom_log ORDER BY job_id, extra;
 job_id |         args          |              extra              |      runner       
--------+-----------------------+---------------------------------+-------------------
   1001 | {"type": "function"}  | custom_func                     | default_perm_user
   1002 | {"type": "procedure"} | custom_proc                     | default_perm_user
   1003 | {"type": "procedure"} | custom_proc2 1 COMMIT procedure | default_perm_user
   1003 | {"type": "procedure"} | custom_proc2 3 COMMIT procedure | default_perm_user
   1004 | {"type": "function"}  | custom_func                     | default_perm_user
   1005 | {"type": "function"}  | security definer                | default_perm_user

\set ON_ERROR_STOP 0
-- test bad input
SELECT delete_job(NULL);
 delete_job 
------------
 

SELECT delete_job(-1);
ERROR:  job -1 not found
\set ON_ERROR_STOP 1
-- We keep job 1001 for some additional checks.
SELECT delete_job(1002);
 delete_job 
------------
 

SELECT delete_job(1003);
 delete_job 
------------
 

SELECT delete_job(1004);
 delete_job 
------------
 

SELECT delete_job(1005);
 delete_job 
------------
 

-- check jobs got removed
SELECT count(*) FROM timescaledb_information.jobs WHERE job_id >= 1002;
 count 
-------
     0

\c :TEST_DBNAME :ROLE_SUPERUSER
-- create a new job with longer id
SELECT nextval('_timescaledb_catalog.bgw_job_id_seq') as nextval \gset
SELECT setval('_timescaledb_catalog.bgw_job_id_seq', 2147483647, false);
   setval   
------------
 2147483647

SELECT add_job('custom_func', '1h', config:='{"type":"function"}'::jsonb, job_name := 'custom_job_name');
  add_job   
------------
 2147483647

\set ON_ERROR_STOP 0
-- test bad input
SELECT alter_job(NULL, if_exists => false);
ERROR:  job ID cannot be NULL
SELECT alter_job(-1, if_exists => false);
ERROR:  job -1 not found
SELECT alter_job(1001, job_name => 'this_is_a_really_really_really_long_application_name_to_overflow');
ERROR:  application name too long.
SELECT alter_job(2147483647, job_name => 'this_is_a_really_really_really_long_application_name_to_overflow');
ERROR:  application name too long.
\set ON_ERROR_STOP 1
-- test bad input but don't fail
SELECT alter_job(NULL, if_exists => true);
NOTICE:  job 0 not found, skipping
 alter_job 
-----------
 

SELECT alter_job(-1, if_exists => true);
NOTICE:  job -1 not found, skipping
 alter_job 
-----------
 

-- test altering job with NULL config
SELECT job_id FROM alter_job(1001,scheduled:=false);
 job_id 
--------
   1001

SELECT scheduled, config FROM timescaledb_information.jobs WHERE job_id = 1001;
 scheduled |        config        
-----------+----------------------
 f         | {"type": "function"}

-- test updating job settings
SELECT job_id FROM alter_job(1001,config:='{"test":"test"}');
 job_id 
--------
   1001

SELECT scheduled, config FROM timescaledb_information.jobs WHERE job_id = 1001;
 scheduled |      config      
-----------+------------------
 f         | {"test": "test"}

SELECT job_id FROM alter_job(1001,scheduled:=true);
 job_id 
--------
   1001

SELECT scheduled, config FROM timescaledb_information.jobs WHERE job_id = 1001;
 scheduled |      config      
-----------+------------------
 t         | {"test": "test"}

SELECT job_id FROM alter_job(1001,scheduled:=false);
 job_id 
--------
   1001

SELECT scheduled, config FROM timescaledb_information.jobs WHERE job_id = 1001;
 scheduled |      config      
-----------+------------------
 f         | {"test": "test"}

-- test updating the job name
SELECT job_id, application_name FROM alter_job(1001,job_name:='custom_name_2');
 job_id |   application_name   
--------+----------------------
   1001 | custom_name_2 [1001]

SELECT job_id, application_name FROM alter_job(2147483647,job_name:='short_name_to_fit');
   job_id   |        application_name        
------------+--------------------------------
 2147483647 | short_name_to_fit [2147483647]

SELECT application_name FROM timescaledb_information.jobs WHERE job_id >= 1001;
        application_name        
--------------------------------
 custom_name_2 [1001]
 short_name_to_fit [2147483647]

-- Done with jobs now, so remove it.
SELECT delete_job(1001);
 delete_job 
------------
 

SELECT delete_job(2147483647);
 delete_job 
------------
 

-- reset the sequence to its previous value
SELECT setval('_timescaledb_catalog.bgw_job_id_seq', :nextval, false);
 setval 
--------
   1006

--test for #2793
\c :TEST_DBNAME :ROLE_DEFAULT_PERM_USER
-- background workers are disabled, so the job will not run --
SELECT add_job( proc=>'custom_func',
     schedule_interval=>'1h', initial_start =>'2018-01-01 10:00:00-05') AS job_id_1 \gset
SELECT job_id, next_start, scheduled, schedule_interval
FROM timescaledb_information.jobs WHERE job_id > 1001;
 job_id |          next_start          | scheduled | schedule_interval 
--------+------------------------------+-----------+-------------------
   1006 | Mon Jan 01 07:00:00 2018 PST | t         | @ 1 hour

\x
SELECT * FROM timescaledb_information.job_stats WHERE job_id > 1001;
-[ RECORD 1 ]----------+-----------------------------
hypertable_schema      | 
hypertable_name        | 
job_id                 | 1006
last_run_started_at    | -infinity
last_successful_finish | -infinity
last_run_status        | 
job_status             | Scheduled
last_run_duration      | 
next_start             | Mon Jan 01 07:00:00 2018 PST
total_runs             | 0
total_successes        | 0
total_failures         | 0

\x
SELECT delete_job(:job_id_1);
 delete_job 
------------
 

-- tests for #3545
TRUNCATE custom_log;
-- Nested procedure call
CREATE OR REPLACE PROCEDURE custom_proc_nested(job_id int, args jsonb) LANGUAGE PLPGSQL AS
$$
BEGIN
  INSERT INTO custom_log VALUES($1, $2, 'custom_proc_nested 1 COMMIT');
  COMMIT;
  INSERT INTO custom_log VALUES($1, $2, 'custom_proc_nested 2 ROLLBACK');
  ROLLBACK;
  INSERT INTO custom_log VALUES($1, $2, 'custom_proc_nested 3 COMMIT');
  COMMIT;
END
$$;
CREATE OR REPLACE PROCEDURE custom_proc3(job_id int, args jsonb) LANGUAGE PLPGSQL AS
$$
BEGIN
    CALL custom_proc_nested(job_id, args);
END
$$;
CREATE OR REPLACE PROCEDURE custom_proc4(job_id int, args jsonb) LANGUAGE PLPGSQL AS
$$
BEGIN
    INSERT INTO custom_log VALUES($1, $2, 'custom_proc4 1 COMMIT');
    COMMIT;
    INSERT INTO custom_log VALUES($1, $2, 'custom_proc4 2 ROLLBACK');
    ROLLBACK;
    RAISE EXCEPTION 'forced exception';
    INSERT INTO custom_log VALUES($1, $2, 'custom_proc4 3 ABORT');
    COMMIT;
END
$$;
CREATE OR REPLACE PROCEDURE custom_proc5(job_id int, args jsonb) LANGUAGE PLPGSQL AS
$$
BEGIN
    CALL refresh_continuous_aggregate('conditions_summary_daily', '2021-08-01 00:00', '2021-08-31 00:00');
END
$$;
-- Remove any default jobs, e.g., telemetry
\c :TEST_DBNAME :ROLE_SUPERUSER
TRUNCATE _timescaledb_catalog.bgw_job RESTART IDENTITY CASCADE;
NOTICE:  truncate cascades to table "bgw_job_stat"
NOTICE:  truncate cascades to table "bgw_policy_chunk_stats"
\c :TEST_DBNAME :ROLE_DEFAULT_PERM_USER
SELECT add_job('custom_proc2', '1h', config := '{"type":"procedure"}'::jsonb, initial_start := now()) AS job_id_1 \gset
SELECT add_job('custom_proc3', '1h', config := '{"type":"procedure"}'::jsonb, initial_start := now()) AS job_id_2 \gset
\c :TEST_DBNAME :ROLE_SUPERUSER
-- Start Background Workers
SELECT _timescaledb_functions.start_background_workers();
 start_background_workers 
--------------------------
 t

-- Wait for jobs
SELECT test.wait_for_job_to_run(:job_id_1, 1);
 wait_for_job_to_run 
---------------------
 t

SELECT test.wait_for_job_to_run(:job_id_2, 1);
 wait_for_job_to_run 
---------------------
 t

-- Check results
SELECT * FROM custom_log ORDER BY job_id, extra;
 job_id |         args          |              extra              |      runner       
--------+-----------------------+---------------------------------+-------------------
   1000 | {"type": "procedure"} | custom_proc2 1 COMMIT procedure | default_perm_user
   1000 | {"type": "procedure"} | custom_proc2 3 COMMIT procedure | default_perm_user
   1001 | {"type": "procedure"} | custom_proc_nested 1 COMMIT     | default_perm_user
   1001 | {"type": "procedure"} | custom_proc_nested 3 COMMIT     | default_perm_user

-- Delete previous jobs
SELECT delete_job(:job_id_1);
 delete_job 
------------
 

SELECT delete_job(:job_id_2);
 delete_job 
------------
 

TRUNCATE custom_log;
-- Forced Exception
SELECT add_job('custom_proc4', '1h', config := '{"type":"procedure"}'::jsonb, initial_start := now()) AS job_id_3 \gset
SELECT _timescaledb_functions.restart_background_workers();
 restart_background_workers 
----------------------------
 t

SELECT test.wait_for_job_to_run(:job_id_3, 1);
INFO:  wait_for_job_to_run: job execution failed
 wait_for_job_to_run 
---------------------
 f

-- Check results
SELECT * FROM custom_log ORDER BY job_id, extra;
 job_id |         args          |         extra         |   runner   
--------+-----------------------+-----------------------+------------
   1002 | {"type": "procedure"} | custom_proc4 1 COMMIT | super_user

-- Delete previous jobs
SELECT delete_job(:job_id_3);
 delete_job 
------------
 

CREATE TABLE conditions (
  time TIMESTAMP NOT NULL,
  location TEXT NOT NULL,
  location2 char(10) NOT NULL,
  temperature DOUBLE PRECISION NULL,
  humidity DOUBLE PRECISION NULL
) WITH (autovacuum_enabled = FALSE);
SELECT create_hypertable('conditions', 'time', chunk_time_interval := '15 days'::interval);
WARNING:  column type "timestamp without time zone" used for "time" does not follow best practices
    create_hypertable    
-------------------------
 (1,public,conditions,t)

ALTER TABLE conditions
  SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'location',
    timescaledb.compress_orderby = 'time'
);
INSERT INTO conditions
SELECT generate_series('2021-08-01 00:00'::timestamp, '2021-08-31 00:00'::timestamp, '1 day'), 'POR', 'klick', 55, 75;
-- Chunk compress stats
SELECT * FROM _timescaledb_internal.compressed_chunk_stats ORDER BY chunk_name;
 hypertable_schema | hypertable_name |     chunk_schema      |    chunk_name    | compression_status | uncompressed_heap_size | uncompressed_index_size | uncompressed_toast_size | uncompressed_total_size | compressed_heap_size | compressed_index_size | compressed_toast_size | compressed_total_size 
-------------------+-----------------+-----------------------+------------------+--------------------+------------------------+-------------------------+-------------------------+-------------------------+----------------------+-----------------------+-----------------------+-----------------------
 public            | conditions      | _timescaledb_internal | _hyper_1_1_chunk | Uncompressed       |                        |                         |                         |                         |                      |                       |                       |                      
 public            | conditions      | _timescaledb_internal | _hyper_1_2_chunk | Uncompressed       |                        |                         |                         |                         |                      |                       |                       |                      
 public            | conditions      | _timescaledb_internal | _hyper_1_3_chunk | Uncompressed       |                        |                         |                         |                         |                      |                       |                       |                      

-- Compression policy
SELECT add_compression_policy('conditions', interval '1 day') AS job_id_4 \gset
SELECT _timescaledb_functions.restart_background_workers();
 restart_background_workers 
----------------------------
 t

SELECT test.wait_for_job_to_run(:job_id_4, 1);
 wait_for_job_to_run 
---------------------
 t

-- Chunk compress stats
SELECT * FROM _timescaledb_internal.compressed_chunk_stats ORDER BY chunk_name;
 hypertable_schema | hypertable_name |     chunk_schema      |    chunk_name    | compression_status | uncompressed_heap_size | uncompressed_index_size | uncompressed_toast_size | uncompressed_total_size | compressed_heap_size | compressed_index_size | compressed_toast_size | compressed_total_size 
-------------------+-----------------+-----------------------+------------------+--------------------+------------------------+-------------------------+-------------------------+-------------------------+----------------------+-----------------------+-----------------------+-----------------------
 public            | conditions      | _timescaledb_internal | _hyper_1_1_chunk | Compressed         |                   8192 |                   16384 |                    8192 |                   32768 |                16384 |                 16384 |                  8192 |                 40960
 public            | conditions      | _timescaledb_internal | _hyper_1_2_chunk | Compressed         |                   8192 |                   16384 |                    8192 |                   32768 |                16384 |                 16384 |                  8192 |                 40960
 public            | conditions      | _timescaledb_internal | _hyper_1_3_chunk | Compressed         |                   8192 |                   16384 |                    8192 |                   32768 |                16384 |                 16384 |                  8192 |                 40960

--TEST compression job after inserting data into previously compressed chunk
INSERT INTO conditions
SELECT generate_series('2021-08-01 00:00'::timestamp, '2021-08-31 00:00'::timestamp, '1 day'), 'NYC', 'nycity', 40, 40;
SELECT id, table_name, status from _timescaledb_catalog.chunk
where hypertable_id = (select id from _timescaledb_catalog.hypertable
                       where table_name = 'conditions')
order by id;
 id |    table_name    | status 
----+------------------+--------
  1 | _hyper_1_1_chunk |      9
  2 | _hyper_1_2_chunk |      9
  3 | _hyper_1_3_chunk |      9

--running job second time, wait for it to complete
select t.schedule_interval FROM alter_job(:job_id_4, next_start=> now() ) t;
 schedule_interval 
-------------------
 @ 12 hours

SELECT _timescaledb_functions.restart_background_workers();
 restart_background_workers 
----------------------------
 t

SELECT test.wait_for_job_to_run(:job_id_4, 2);
 wait_for_job_to_run 
---------------------
 t

SELECT id, table_name, status from _timescaledb_catalog.chunk
where hypertable_id = (select id from _timescaledb_catalog.hypertable
                       where table_name = 'conditions')
order by id;
 id |    table_name    | status 
----+------------------+--------
  1 | _hyper_1_1_chunk |      1
  2 | _hyper_1_2_chunk |      1
  3 | _hyper_1_3_chunk |      1

-- Drop the compression job
SELECT delete_job(:job_id_4);
 delete_job 
------------
 

-- Decompress chunks before create the cagg
SELECT decompress_chunk(c) FROM show_chunks('conditions') c;
            decompress_chunk            
----------------------------------------
 _timescaledb_internal._hyper_1_1_chunk
 _timescaledb_internal._hyper_1_2_chunk
 _timescaledb_internal._hyper_1_3_chunk

-- TEST Continuous Aggregate job
CREATE MATERIALIZED VIEW conditions_summary_daily
WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS
SELECT location,
   time_bucket(INTERVAL '1 day', time) AS bucket,
   AVG(temperature),
   MAX(temperature),
   MIN(temperature)
FROM conditions
GROUP BY location, bucket
WITH NO DATA;
-- Refresh Continous Aggregate by Job
SELECT add_job('custom_proc5', '1h', config := '{"type":"procedure"}'::jsonb, initial_start := now()) AS job_id_5 \gset
SELECT _timescaledb_functions.restart_background_workers();
 restart_background_workers 
----------------------------
 t

SELECT test.wait_for_job_to_run(:job_id_5, 1);
 wait_for_job_to_run 
---------------------
 t

SELECT count(*) FROM conditions_summary_daily;
 count 
-------
    62

-- TESTs for alter_job_set_hypertable_id API
SELECT _timescaledb_functions.alter_job_set_hypertable_id( :job_id_5, NULL);
 alter_job_set_hypertable_id 
-----------------------------
                        1004

SELECT id, proc_name, hypertable_id
FROM _timescaledb_catalog.bgw_job WHERE id = :job_id_5;
  id  |  proc_name   | hypertable_id 
------+--------------+---------------
 1004 | custom_proc5 |              

-- error case, try to associate with a PG relation
\set ON_ERROR_STOP 0
SELECT _timescaledb_functions.alter_job_set_hypertable_id( :job_id_5, 'custom_log');
ERROR:  relation "custom_log" is not a hypertable or continuous aggregate
\set ON_ERROR_STOP 1
-- TEST associate the cagg with the job
SELECT _timescaledb_functions.alter_job_set_hypertable_id( :job_id_5, 'conditions_summary_daily'::regclass);
 alter_job_set_hypertable_id 
-----------------------------
                        1004

SELECT id, proc_name, hypertable_id
FROM _timescaledb_catalog.bgw_job WHERE id = :job_id_5;
  id  |  proc_name   | hypertable_id 
------+--------------+---------------
 1004 | custom_proc5 |             3

--verify that job is dropped when cagg is dropped
DROP MATERIALIZED VIEW conditions_summary_daily;
NOTICE:  drop cascades to table _timescaledb_internal._hyper_3_7_chunk
SELECT id, proc_name, hypertable_id
FROM _timescaledb_catalog.bgw_job WHERE id = :job_id_5;
 id | proc_name | hypertable_id 
----+-----------+---------------

-- Cleanup
DROP TABLE conditions;
DROP TABLE custom_log;
-- Stop Background Workers
SELECT _timescaledb_functions.stop_background_workers();
 stop_background_workers 
-------------------------
 t

SELECT _timescaledb_functions.restart_background_workers();
 restart_background_workers 
----------------------------
 t

\set ON_ERROR_STOP 0
-- add test for custom jobs with custom check functions
-- create the functions/procedures to be used as checking functions
CREATE OR REPLACE PROCEDURE test_config_check_proc(config jsonb)
LANGUAGE PLPGSQL
AS $$
DECLARE
  drop_after interval;
BEGIN
    SELECT jsonb_object_field_text (config, 'drop_after')::interval INTO STRICT drop_after;
    IF drop_after IS NULL THEN
        RAISE EXCEPTION 'Config must be not NULL and have drop_after';
    END IF ;
END
$$;
CREATE OR REPLACE FUNCTION test_config_check_func(config jsonb) RETURNS VOID
AS $$
DECLARE
  drop_after interval;
BEGIN
    IF config IS NULL THEN
        RETURN;
    END IF;
    SELECT jsonb_object_field_text (config, 'drop_after')::interval INTO STRICT drop_after;
    IF drop_after IS NULL THEN
        RAISE EXCEPTION 'Config can be NULL but must have drop_after if not';
    END IF ;
END
$$ LANGUAGE PLPGSQL;
-- step 2, create a procedure to run as a custom job
CREATE OR REPLACE PROCEDURE test_proc_with_check(job_id int, config jsonb)
LANGUAGE PLPGSQL
AS $$
BEGIN
  RAISE NOTICE 'Will only print this if config passes checks, my config is %', config;
END
$$;
-- step 3, add the job with the config check function passed as argument
-- test procedures, should get an unsupported error
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'test_config_check_proc'::regproc);
ERROR:  unsupported function type
-- test functions
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'test_config_check_func'::regproc);
ERROR:  Config can be NULL but must have drop_after if not
select add_job('test_proc_with_check', '5 secs', config => NULL, check_config => 'test_config_check_func'::regproc);
 add_job 
---------
    1005

select add_job('test_proc_with_check', '5 secs', config => '{"drop_after": "chicken"}', check_config => 'test_config_check_func'::regproc);
ERROR:  invalid input syntax for type interval: "chicken"
select add_job('test_proc_with_check', '5 secs', config => '{"drop_after": "2 weeks"}', check_config => 'test_config_check_func'::regproc)
as job_with_func_check_id \gset
--- test alter_job
select alter_job(:job_with_func_check_id, config => '{"drop_after":"chicken"}');
ERROR:  invalid input syntax for type interval: "chicken"
select config from alter_job(:job_with_func_check_id, config => '{"drop_after":"5 years"}');
          config           
---------------------------
 {"drop_after": "5 years"}

-- test that jobs with an incorrect check function signature will not be registered
-- these are all incorrect function signatures
CREATE OR REPLACE FUNCTION test_config_check_func_0args() RETURNS VOID
AS $$
BEGIN
    RAISE NOTICE 'I take no arguments and will validate anything you give me!';
END
$$ LANGUAGE PLPGSQL;
CREATE OR REPLACE FUNCTION test_config_check_func_2args(config jsonb, intarg int) RETURNS VOID
AS $$
BEGIN
    RAISE NOTICE 'I take two arguments (jsonb, int) and I should fail to run!';
END
$$ LANGUAGE PLPGSQL;
CREATE OR REPLACE FUNCTION test_config_check_func_intarg(config int) RETURNS VOID
AS $$
BEGIN
    RAISE NOTICE 'I take one argument which is an integer and I should fail to run!';
END
$$ LANGUAGE PLPGSQL;
-- -- this should fail, it has an incorrect check function
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'test_config_check_func_0args'::regproc);
ERROR:  function or procedure public.test_config_check_func_0args(config jsonb) not found
-- -- so should this
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'test_config_check_func_2args'::regproc);
ERROR:  function or procedure public.test_config_check_func_2args(config jsonb) not found
-- and this
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'test_config_check_func_intarg'::regproc);
ERROR:  function or procedure public.test_config_check_func_intarg(config jsonb) not found
-- and this fails as it calls a nonexistent function
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'test_nonexistent_check_func'::regproc);
ERROR:  function "test_nonexistent_check_func" does not exist at character 82
-- when called with a valid check function and a NULL config no check should occur
CREATE OR REPLACE FUNCTION test_config_check_func(config jsonb) RETURNS VOID
AS $$
BEGIN
    RAISE NOTICE 'This message will get printed for both NULL and not NULL config';
END
$$ LANGUAGE PLPGSQL;
SET client_min_messages = NOTICE;
-- check done for both NULL and non-NULL config
select add_job('test_proc_with_check', '5 secs', config => NULL, check_config => 'test_config_check_func'::regproc);
NOTICE:  This message will get printed for both NULL and not NULL config
 add_job 
---------
    1007

-- check done
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'test_config_check_func'::regproc) as job_id \gset
NOTICE:  This message will get printed for both NULL and not NULL config
-- check function not returning void
CREATE OR REPLACE FUNCTION test_config_check_func_returns_int(config jsonb) RETURNS INT
AS $$
BEGIN
    raise notice 'I print a message, and then I return least(1,2)';
    RETURN LEAST(1, 2);
END
$$ LANGUAGE PLPGSQL;
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'test_config_check_func_returns_int'::regproc,
initial_start => :'time_zero'::timestamptz) as job_id_int \gset
NOTICE:  I print a message, and then I return least(1,2)
-- rename the check function and then call alter_job to register the new name
ALTER FUNCTION test_config_check_func RENAME TO renamed_func;
select job_id, schedule_interval, config, check_config from alter_job(:job_id, check_config => 'renamed_func'::regproc, schedule_interval => '1 hour');
NOTICE:  This message will get printed for both NULL and not NULL config
 job_id | schedule_interval | config |    check_config     
--------+-------------------+--------+---------------------
   1008 | @ 1 hour          | {}     | public.renamed_func

-- run alter again, should get a config check
select job_id, schedule_interval, config, check_config from alter_job(:job_id, config => '{}');
NOTICE:  This message will get printed for both NULL and not NULL config
 job_id | schedule_interval | config |    check_config     
--------+-------------------+--------+---------------------
   1008 | @ 1 hour          | {}     | public.renamed_func

-- drop the registered check function, verify that alter_job will work and print a warning that
-- the check is being skipped due to the check function missing
DROP FUNCTION test_config_check_func_returns_int;
select job_id, schedule_interval, config, check_config from alter_job(:job_id_int, config => '{"field":"value"}');
WARNING:  function public.test_config_check_func_returns_int(config jsonb) not found, skipping config validation for job 1009
 job_id | schedule_interval |       config       |               check_config                
--------+-------------------+--------------------+-------------------------------------------
   1009 | @ 5 secs          | {"field": "value"} | public.test_config_check_func_returns_int

-- do not drop the current check function but register a new one
CREATE OR REPLACE FUNCTION substitute_check_func(config jsonb) RETURNS VOID
AS $$
BEGIN
    RAISE NOTICE 'This message is a substitute of the previously printed one';
END
$$ LANGUAGE PLPGSQL;
-- register the new check
select job_id, schedule_interval, config, check_config from alter_job(:job_id, check_config => 'substitute_check_func');
NOTICE:  This message is a substitute of the previously printed one
 job_id | schedule_interval | config |         check_config         
--------+-------------------+--------+------------------------------
   1008 | @ 1 hour          | {}     | public.substitute_check_func

select job_id, schedule_interval, config, check_config from alter_job(:job_id, config => '{}');
NOTICE:  This message is a substitute of the previously printed one
 job_id | schedule_interval | config |         check_config         
--------+-------------------+--------+------------------------------
   1008 | @ 1 hour          | {}     | public.substitute_check_func

RESET client_min_messages;
-- test an oid that doesn't exist
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 17424217::regproc);
ERROR:  function with OID 17424217 does not exist
\c :TEST_DBNAME :ROLE_SUPERUSER
-- test a function with insufficient privileges
create schema test_schema;
create role user_noexec with login;
grant usage on schema test_schema to user_noexec;
CREATE OR REPLACE FUNCTION test_schema.test_config_check_func_privileges(config jsonb) RETURNS VOID
AS $$
BEGIN
    RAISE NOTICE 'This message will only get printed if privileges suffice';
END
$$ LANGUAGE PLPGSQL;
revoke execute on function test_schema.test_config_check_func_privileges from public;
-- verify the user doesn't have execute permissions on the function
select has_function_privilege('user_noexec', 'test_schema.test_config_check_func_privileges(jsonb)', 'execute');
 has_function_privilege 
------------------------
 f

\c :TEST_DBNAME user_noexec
-- user_noexec should not have exec permissions on this function
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'test_schema.test_config_check_func_privileges'::regproc);
ERROR:  permission denied for function "test_config_check_func_privileges"
\c :TEST_DBNAME :ROLE_SUPERUSER
-- check that alter_job rejects a check function with invalid signature
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'renamed_func',
initial_start => :'time_zero'::timestamptz) as job_id_alter \gset
NOTICE:  This message will get printed for both NULL and not NULL config
select job_id, schedule_interval, config, check_config from alter_job(:job_id_alter, check_config => 'test_config_check_func_0args');
ERROR:  function or procedure public.test_config_check_func_0args(config jsonb) not found
select job_id, schedule_interval, config, check_config from alter_job(:job_id_alter);
NOTICE:  This message will get printed for both NULL and not NULL config
 job_id | schedule_interval | config |    check_config     
--------+-------------------+--------+---------------------
   1010 | @ 5 secs          | {}     | public.renamed_func

-- test that we can unregister the check function
select job_id, schedule_interval, config, check_config from alter_job(:job_id_alter, check_config => 0);
 job_id | schedule_interval | config | check_config 
--------+-------------------+--------+--------------
   1010 | @ 5 secs          | {}     | 

-- no message printed now
select job_id, schedule_interval, config, check_config from alter_job(:job_id_alter, config => '{}');
 job_id | schedule_interval | config | check_config 
--------+-------------------+--------+--------------
   1010 | @ 5 secs          | {}     | 

-- test the case where we have a background job that registers jobs with a check fn
CREATE OR REPLACE PROCEDURE add_scheduled_jobs_with_check(job_id int, config jsonb) LANGUAGE PLPGSQL AS
$$
BEGIN
    perform add_job('test_proc_with_check', schedule_interval => '10 secs', config => '{}', check_config => 'renamed_func');
END
$$;
select add_job('add_scheduled_jobs_with_check', schedule_interval => '1 hour') as last_job_id \gset
-- wait for enough time
SELECT _timescaledb_functions.restart_background_workers();
 restart_background_workers 
----------------------------
 t

SELECT test.wait_for_job_to_run(:last_job_id, 1);
 wait_for_job_to_run 
---------------------
 t

select total_runs, total_successes, last_run_status from timescaledb_information.job_stats where job_id = :last_job_id;
 total_runs | total_successes | last_run_status 
------------+-----------------+-----------------
          1 |               1 | Success

-- test coverage for alter_job
-- registering an invalid oid
select alter_job(:job_id_alter, check_config => 123456789::regproc);
ERROR:  function with OID 123456789 does not exist
-- registering a function with insufficient privileges
\c :TEST_DBNAME user_noexec
select * from add_job('test_proc_with_check', '5 secs', config => '{}') as job_id_owner \gset
select * from alter_job(:job_id_owner, check_config => 'test_schema.test_config_check_func_privileges'::regproc);
ERROR:  permission denied for function "test_config_check_func_privileges"
\c :TEST_DBNAME :ROLE_SUPERUSER
DROP SCHEMA test_schema CASCADE;
NOTICE:  drop cascades to function test_schema.test_config_check_func_privileges(jsonb)
-- Delete all jobs with that owner before we can drop the user.
DELETE FROM _timescaledb_catalog.bgw_job WHERE owner = 'user_noexec'::regrole;
DROP ROLE user_noexec;
-- test with aggregate check proc
create function jsonb_add (j1 jsonb, j2 jsonb) returns jsonb
AS $$
BEGIN
    RETURN j1 || j2;
END
$$ LANGUAGE PLPGSQL;
CREATE AGGREGATE sum_jsb (jsonb)
(
    sfunc = jsonb_add,
    stype = jsonb,
    initcond = '{}'
);
-- for test coverage, check unsupported aggregate type
select add_job('test_proc_with_check', '5 secs', config => '{}', check_config => 'sum_jsb'::regproc);
ERROR:  unsupported function type
-- Cleanup jobs
TRUNCATE _timescaledb_catalog.bgw_job CASCADE;
NOTICE:  truncate cascades to table "bgw_job_stat"
NOTICE:  truncate cascades to table "bgw_policy_chunk_stats"
-- github issue 4610
CREATE TABLE sensor_data
(
    time timestamptz not null,
    sensor_id integer not null,
    cpu double precision null,
    temperature double precision null
);
SELECT FROM create_hypertable('sensor_data','time');
--

SELECT '2022-10-06 00:00:00+00' as start_date_sd \gset
INSERT INTO sensor_data
	SELECT
		time + (INTERVAL '1 minute' * random()) AS time,
		sensor_id,
		random() AS cpu,
		random()* 100 AS temperature
	FROM
		generate_series(:'start_date_sd'::timestamptz - INTERVAL '1 months', :'start_date_sd'::timestamptz - INTERVAL '1 week', INTERVAL '30 minute') AS g1(time),
		generate_series(1, 50, 1 ) AS g2(sensor_id)
	ORDER BY
		time;
-- enable compression
ALTER TABLE sensor_data SET (timescaledb.compress, timescaledb.compress_orderby = 'time DESC');
-- create new chunks
INSERT INTO sensor_data
	SELECT
		time + (INTERVAL '1 minute' * random()) AS time,
		sensor_id,
		random() AS cpu,
		random()* 100 AS temperature
	FROM
		generate_series(:'start_date_sd'::timestamptz - INTERVAL '2 months', :'start_date_sd'::timestamptz - INTERVAL '2 week', INTERVAL '60 minute') AS g1(time),
		generate_series(1, 30, 1 ) AS g2(sensor_id)
	ORDER BY
		time;
-- get the name of a new uncompressed chunk
SELECT chunk_name AS new_uncompressed_chunk_name
  FROM timescaledb_information.chunks
  WHERE hypertable_name = 'sensor_data' AND NOT is_compressed LIMIT 1 \gset
-- change compression status so that this chunk is skipped when policy is run
update _timescaledb_catalog.chunk set status=3 where table_name = :'new_uncompressed_chunk_name';
-- add new compression policy job
SELECT add_compression_policy('sensor_data', INTERVAL '1' minute) AS compressjob_id \gset
-- set recompress to true
SELECT alter_job(id,config:=jsonb_set(config,'{recompress}', 'true')) FROM _timescaledb_catalog.bgw_job WHERE id = :compressjob_id;
                                                                                                      alter_job                                                                                                      
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 (1014,"@ 12 hours","@ 0",-1,"@ 1 hour",t,"{""recompress"": true, ""hypertable_id"": 4, ""compress_after"": ""@ 1 min""}",-infinity,_timescaledb_functions.policy_compression_check,f,,,"Columnstore Policy [1014]")

-- verify that there are other uncompressed new chunks that need to be compressed
SELECT count(*) > 1
  FROM timescaledb_information.chunks
  WHERE hypertable_name = 'sensor_data' AND NOT is_compressed;
 ?column? 
----------
 t

-- disable notice/warning as the new_uncompressed_chunk_name
-- is dynamic and it will be printed in those messages.
SET client_min_messages TO ERROR;
CALL run_job(:compressjob_id);
ERROR:  columnstore policy failure
SET client_min_messages TO NOTICE;
-- check compression status is not changed for the chunk whose status was manually updated
SELECT status FROM _timescaledb_catalog.chunk where table_name = :'new_uncompressed_chunk_name';
 status 
--------
      3

-- confirm all the other new chunks are now compressed despite
-- facing an error when trying to compress :'new_uncompressed_chunk_name'
SELECT count(*) = 0
  FROM timescaledb_information.chunks
  WHERE hypertable_name = 'sensor_data' AND NOT is_compressed;
 ?column? 
----------
 t

-- cleanup
SELECT _timescaledb_functions.stop_background_workers();
 stop_background_workers 
-------------------------
 t

DROP TABLE sensor_data;
SELECT _timescaledb_functions.restart_background_workers();
 restart_background_workers 
----------------------------
 t

-- Github issue #5537
-- Proc that waits until the given job enters the expected state
CREATE OR REPLACE PROCEDURE wait_for_job_status(job_param_id INTEGER, expected_status TEXT, spins INTEGER=:TEST_SPINWAIT_ITERS)
LANGUAGE PLPGSQL AS $$
DECLARE
  jobstatus TEXT;
BEGIN
  FOR i in 1..spins
  LOOP
    SELECT job_status FROM timescaledb_information.job_stats WHERE job_id = job_param_id INTO jobstatus;
    IF jobstatus = expected_status THEN
      RETURN;
    END IF;
    PERFORM pg_sleep(0.1);
    ROLLBACK;
  END LOOP;
  RAISE EXCEPTION 'wait_for_job_status(%): timeout after % tries', job_param_id, spins;
END;
$$;
-- Proc that sleeps for 1m - to keep the test jobs in running state
CREATE OR REPLACE PROCEDURE proc_that_sleeps(job_id INT, config JSONB)
LANGUAGE PLPGSQL AS
$$
BEGIN
    PERFORM pg_sleep(60);
END
$$;
-- create new jobs and ensure that the second one gets scheduled
-- before the first one by adjusting the initial_start values
SELECT add_job('proc_that_sleeps', '1h', initial_start => now()::timestamptz + interval '2s') AS job_id_1 \gset
SELECT add_job('proc_that_sleeps', '1h', initial_start => now()::timestamptz - interval '2s') AS job_id_2 \gset
SELECT _timescaledb_functions.restart_background_workers();
 restart_background_workers 
----------------------------
 t

-- wait for the jobs to start running job_2 will start running first
CALL wait_for_job_status(:job_id_2, 'Running');
CALL wait_for_job_status(:job_id_1, 'Running');
-- add a new job and wait for it to start
SELECT add_job('proc_that_sleeps', '1h') AS job_id_3 \gset
CALL wait_for_job_status(:job_id_3, 'Running');
-- verify that none of the jobs crashed
SELECT job_id, job_status, next_start,
       total_runs, total_successes, total_failures
  FROM timescaledb_information.job_stats
  WHERE job_id IN (:job_id_1, :job_id_2, :job_id_3)
  ORDER BY job_id;
 job_id | job_status | next_start | total_runs | total_successes | total_failures 
--------+------------+------------+------------+-----------------+----------------
   1015 | Running    | -infinity  |          1 |               0 |              0
   1016 | Running    | -infinity  |          1 |               0 |              0
   1017 | Running    | -infinity  |          1 |               0 |              0

SELECT job_id, err_message
  FROM timescaledb_information.job_errors
  WHERE job_id IN (:job_id_1, :job_id_2, :job_id_3);
 job_id | err_message 
--------+-------------

-- cleanup
SELECT _timescaledb_functions.stop_background_workers();
 stop_background_workers 
-------------------------
 t

CALL wait_for_job_status(:job_id_1, 'Scheduled');
CALL wait_for_job_status(:job_id_2, 'Scheduled');
CALL wait_for_job_status(:job_id_3, 'Scheduled');
SELECT delete_job(:job_id_1);
 delete_job 
------------
 

SELECT delete_job(:job_id_2);
 delete_job 
------------
 

SELECT delete_job(:job_id_3);
 delete_job 
------------
 

CREATE OR REPLACE FUNCTION ts_test_bgw_job_function_call_string(job_id INTEGER) RETURNS text
AS :MODULE_PATHNAME LANGUAGE C STABLE STRICT;
\set ON_ERROR_STOP 0
SELECT ts_test_bgw_job_function_call_string(999999);
ERROR:  job 999999 not found
\set ON_ERROR_STOP 1
SELECT add_job('custom_func', '1h') AS job_func \gset
SELECT add_job('custom_proc', '1h') AS job_proc \gset
SELECT ts_test_bgw_job_function_call_string(:job_func);
  ts_test_bgw_job_function_call_string   
-----------------------------------------
 SELECT public.custom_func('1018', NULL)

SELECT ts_test_bgw_job_function_call_string(:job_proc);
 ts_test_bgw_job_function_call_string  
---------------------------------------
 CALL public.custom_proc('1019', NULL)

SELECT delete_job(:job_func);
 delete_job 
------------
 

SELECT delete_job(:job_proc);
 delete_job 
------------
 

SELECT add_job('custom_func', '1h', config => '{"type":"function"}'::jsonb) AS job_func \gset
SELECT add_job('custom_proc', '1h', config => '{"type":"procedure"}'::jsonb) AS job_proc \gset
SELECT ts_test_bgw_job_function_call_string(:job_func);
           ts_test_bgw_job_function_call_string            
-----------------------------------------------------------
 SELECT public.custom_func('1020', '{"type": "function"}')

SELECT ts_test_bgw_job_function_call_string(:job_proc);
           ts_test_bgw_job_function_call_string           
----------------------------------------------------------
 CALL public.custom_proc('1021', '{"type": "procedure"}')

-- Remove the procedure and let's check it fallingback to PROKIND_FUNCTION
DROP PROCEDURE custom_proc(jobid int, args jsonb);
SELECT ts_test_bgw_job_function_call_string(:job_proc);
            ts_test_bgw_job_function_call_string            
------------------------------------------------------------
 SELECT public.custom_proc('1021', '{"type": "procedure"}')

\set ON_ERROR_STOP 0
-- Mess with pg catalog to don't identify the PROKIND
BEGIN;
UPDATE pg_catalog.pg_proc SET prokind = 'X' WHERE oid = 'custom_func(int,jsonb)'::regprocedure;
SELECT ts_test_bgw_job_function_call_string(:job_func);
ERROR:  unsupported function type: X
ROLLBACK;
\set ON_ERROR_STOP 1
SELECT delete_job(:job_func);
 delete_job 
------------
 

SELECT delete_job(:job_proc);
 delete_job 
------------
 

-- Test work_mem config option in job execution
CREATE TABLE work_mem_log(job_id int, work_mem_value text);
CREATE OR REPLACE PROCEDURE log_work_mem(job_id int, config jsonb) LANGUAGE PLPGSQL AS
$$
BEGIN
    INSERT INTO work_mem_log VALUES(job_id, current_setting('work_mem'));
END
$$;
-- Add job with work_mem in config
SELECT add_job('log_work_mem', '1h', config => '{"work_mem": "123MB"}'::jsonb) AS job_work_mem \gset
-- Run the job - work_mem should be set to 123MB before execution
CALL run_job(:job_work_mem);
-- Verify work_mem was set during job execution
SELECT * FROM work_mem_log;
 job_id | work_mem_value 
--------+----------------
   1022 | 123MB

-- Cleanup
SELECT delete_job(:job_work_mem);
 delete_job 
------------
 

DROP TABLE work_mem_log;
DROP PROCEDURE log_work_mem;
