----------------------------------------------------------------------
-- File      :   pg-init-template.sql
-- Desc      :   init templates for postgres cluster {{ pg_cluster }}
-- Time      :   {{ '%Y-%m-%d %H:%M' | strftime }}
-- Host      :   {{ pg_instance }} @ {{ inventory_hostname }}:{{ pg_port }}
-- Path      :   /pg/tmp/pg-init-template.sql
-- Note      :   ANSIBLE MANAGED, DO NOT CHANGE!
-- License   :   Apache-2.0 @ https://pigsty.io/docs/about/license/
-- Copyright :   2018-2026  Ruohang Feng / Vonng (rh@vonng.com)
----------------------------------------------------------------------

--==================================================================--
--                           Executions                             --
--==================================================================--
-- {{ pg_bin_dir}}/psql template1 -AXtwqf /pg/tmp/pg-init-template.sql
-- this sql scripts is responsible for post-init procedure
-- it will
--    * create system users such as replicator, monitor user, admin user
--    * create system default roles
--    * create schema, extensions in template1 & postgres
--    * create monitor views in template1 & postgres


--==================================================================--
--                          Default Privileges                      --
--==================================================================--
{% for priv in pg_default_privileges %}
ALTER DEFAULT PRIVILEGES FOR ROLE {{ pg_dbsu }} {{ priv }};
{% endfor %}

{% for priv in pg_default_privileges %}
ALTER DEFAULT PRIVILEGES FOR ROLE {{ pg_admin_username }} {{ priv }};
{% endfor %}

-- for additional business admin, they can SET ROLE to dbrole_admin
{% for priv in pg_default_privileges %}
ALTER DEFAULT PRIVILEGES FOR ROLE "dbrole_admin" {{ priv }};
{% endfor %}

--==================================================================--
--                              Schemas                             --
--==================================================================--
{% for schema in pg_default_schemas %}
{% if schema is string %}
CREATE SCHEMA IF NOT EXISTS "{{ schema }}";
{% elif schema is mapping and 'name' in schema %}
{% if schema.state is defined and schema.state == 'absent' %}
DROP SCHEMA IF EXISTS "{{ schema.name }}" CASCADE;
{% else %}
CREATE SCHEMA IF NOT EXISTS "{{ schema.name }}"{% if 'owner' in schema and schema.owner is not none and schema.owner != '' %} AUTHORIZATION "{{ schema.owner }}"{% endif %};
{% endif %}
{% endif %}
{% endfor %}

-- revoke public creation
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

--==================================================================--
--                             Extensions                           --
--==================================================================--
{% for extension in pg_default_extensions %}
{% if extension is string %}
CREATE EXTENSION IF NOT EXISTS "{{ extension }}" CASCADE;
{% elif extension is mapping and 'name' in extension %}
{% if extension.state is defined and extension.state == 'absent' %}
DROP EXTENSION IF EXISTS "{{ extension.name }}" CASCADE;
{% else %}
CREATE EXTENSION IF NOT EXISTS "{{ extension.name }}"{% if 'schema' in extension %} WITH SCHEMA "{{ extension.schema }}"{% endif %}{% if 'version' in extension %} VERSION '{{ extension.version }}'{% endif %} CASCADE;
{% endif %}
{% endif %}
{% endfor %}

-- always enable file_fdw and default server fs
CREATE EXTENSION IF NOT EXISTS file_fdw;
CREATE SERVER IF NOT EXISTS fs FOREIGN DATA WRAPPER file_fdw;


--==================================================================--
--                          Backup Privileges                       --
--==================================================================--
-- grant backup privileges to replication user
GRANT USAGE ON SCHEMA pg_catalog TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.current_setting(text) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.set_config(text, text, boolean) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_is_in_recovery() TO "{{ pg_replication_username }}";
{% if pg_version|int < 15 %}
GRANT EXECUTE ON FUNCTION pg_catalog.pg_start_backup(text, boolean, boolean) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_stop_backup(boolean, boolean) TO "{{ pg_replication_username }}";
{% endif %}
GRANT EXECUTE ON FUNCTION pg_catalog.pg_create_restore_point(text) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_switch_wal() TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_last_wal_replay_lsn() TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.txid_current() TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.txid_current_snapshot() TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.txid_snapshot_xmax(txid_snapshot) TO "{{ pg_replication_username }}";
GRANT EXECUTE ON FUNCTION pg_catalog.pg_control_checkpoint() TO "{{ pg_replication_username }}";



--==================================================================--
--                            Monitor Schema                        --
--==================================================================--

----------------------------------------------------------------------
-- cleanse & create monitor schema
----------------------------------------------------------------------
CREATE SCHEMA IF NOT EXISTS monitor;
GRANT USAGE ON SCHEMA monitor TO "{{ pg_monitor_username }}";
GRANT USAGE ON SCHEMA monitor TO "{{ pg_admin_username }}";
GRANT USAGE ON SCHEMA monitor TO "{{ pg_replication_username }}";
ALTER USER "{{ pg_monitor_username }}" SET search_path TO monitor,public;


--==================================================================--
--                         Heartbeat Table                          --
--==================================================================--
-- table to hold heartbeat records
DROP TABLE IF EXISTS monitor.heartbeat CASCADE;
CREATE TABLE IF NOT EXISTS monitor.heartbeat
(
    id   VARCHAR(64) PRIMARY KEY,
    ts   TIMESTAMPTZ,
    lsn  BIGINT,
    txid BIGINT
);
COMMENT ON TABLE monitor.heartbeat IS 'heartbeat table, contains one row only';
REVOKE INSERT,UPDATE,DELETE ON TABLE monitor.heartbeat FROM dbrole_readwrite;
GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE monitor.heartbeat TO pg_monitor;

-- function to generate & return generated heartbeat record
CREATE OR REPLACE FUNCTION monitor.upsert_heartbeat()
RETURNS monitor.heartbeat SET search_path = '' AS $$
    INSERT INTO monitor.heartbeat(id, ts, lsn, txid) VALUES (coalesce(current_setting('cluster_name', true), 'unknown'), now(), pg_current_wal_lsn() - '0/0'::PG_LSN, pg_current_xact_id()::text::BIGINT)
    ON CONFLICT(id) DO UPDATE SET ts=EXCLUDED.ts, lsn=EXCLUDED.lsn, txid=EXCLUDED.txid RETURNING *;
$$ LANGUAGE SQL VOLATILE;
COMMENT ON FUNCTION monitor.upsert_heartbeat() IS 'upsert a heartbeat record';

REVOKE ALL ON FUNCTION monitor.upsert_heartbeat() FROM PUBLIC;
REVOKE ALL ON FUNCTION monitor.upsert_heartbeat() FROM dbrole_readonly;
REVOKE ALL ON FUNCTION monitor.upsert_heartbeat() FROM dbrole_offline;
GRANT EXECUTE ON FUNCTION monitor.upsert_heartbeat() TO pg_monitor;

-- function to be used by monitor tools
CREATE OR REPLACE FUNCTION monitor.beating() RETURNS TABLE (cls TEXT, ts TIMESTAMPTZ, lsn PG_LSN, lsn_int BIGINT, txid BIGINT, status TEXT) SET search_path = '' AS $$ SELECT id AS cls, ts , '0/0'::PG_LSN + lsn AS lsn, lsn AS lsn_int, txid, CASE WHEN pg_is_in_recovery() THEN 'recovery' ELSE 'leading' END AS status FROM
(SELECT (CASE WHEN pg_is_in_recovery() THEN (SELECT h FROM monitor.heartbeat h) ELSE monitor.upsert_heartbeat() END).*) d;
$$ LANGUAGE SQL VOLATILE;
COMMENT ON FUNCTION monitor.beating() IS 'refresh heartbeat on leader and return status';

REVOKE ALL ON FUNCTION monitor.beating() FROM PUBLIC;
REVOKE ALL ON FUNCTION monitor.beating() FROM dbrole_readonly;
REVOKE ALL ON FUNCTION monitor.beating() FROM dbrole_offline;
GRANT EXECUTE ON FUNCTION monitor.beating() TO pg_monitor;

-- function to return explain plan of given query
CREATE OR REPLACE FUNCTION monitor.explain(query TEXT) RETURNS JSON
SET search_path = '' AS $$
DECLARE result JSON;
BEGIN
    EXECUTE format('EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) %s', query) INTO result;
    RETURN result;
END; $$ LANGUAGE plpgsql;
COMMENT ON FUNCTION monitor.explain(TEXT) IS 'return verbose explain json plan for given query text';


--==================================================================--
--                          Monitor Views                           --
--==================================================================--

----------------------------------------------------------------------
-- Table bloat estimate : monitor.pg_table_bloat
----------------------------------------------------------------------
DROP FUNCTION IF EXISTS monitor.pg_table_bloat() CASCADE;
DROP FUNCTION IF EXISTS monitor.pg_index_bloat() CASCADE;
DROP VIEW IF EXISTS monitor.pg_table_bloat CASCADE;
DROP VIEW IF EXISTS monitor.pg_index_bloat CASCADE;

-- table bloat func
CREATE OR REPLACE FUNCTION monitor.pg_table_bloat() RETURNS TABLE(datname TEXT,nspname TEXT,relname TEXT,tblid OID,size BIGINT,ratio FLOAT) SET search_path = '' AS
$$SELECT CURRENT_CATALOG AS datname, nspname, relname , tblid , bs * tblpages AS size,
         CASE WHEN tblpages - est_tblpages_ff > 0 THEN (tblpages - est_tblpages_ff)/tblpages::FLOAT ELSE 0 END AS ratio
  FROM (
           SELECT ceil( reltuples / ( (bs-page_hdr)*fillfactor/(tpl_size*100) ) ) + ceil( toasttuples / 4 ) AS est_tblpages_ff,
                  tblpages, fillfactor, bs, tblid, nspname, relname, is_na
           FROM (
                    SELECT
                        ( 4 + tpl_hdr_size + tpl_data_size + (2 * ma)
                            - CASE WHEN tpl_hdr_size % ma = 0 THEN ma ELSE tpl_hdr_size % ma END
                            - CASE WHEN ceil(tpl_data_size)::INT % ma = 0 THEN ma ELSE ceil(tpl_data_size)::INT % ma END
                            ) AS tpl_size, (heappages + toastpages) AS tblpages, heappages,
                        toastpages, reltuples, toasttuples, bs, page_hdr, tblid, nspname, relname, fillfactor, is_na
                    FROM (
                             SELECT
                                 tbl.oid AS tblid, ns.nspname , tbl.relname, tbl.reltuples,
                                 tbl.relpages AS heappages, coalesce(toast.relpages, 0) AS toastpages,
                                 coalesce(toast.reltuples, 0) AS toasttuples,
                                 coalesce(substring(array_to_string(tbl.reloptions, ' ') FROM 'fillfactor=([0-9]+)')::smallint, 100) AS fillfactor,
                                 current_setting('block_size')::numeric AS bs,
                                 CASE WHEN version()~'mingw32' OR version()~'64-bit|x86_64|ppc64|ia64|amd64' THEN 8 ELSE 4 END AS ma,
                                 24 AS page_hdr,
                                 23 + CASE WHEN MAX(coalesce(s.null_frac,0)) > 0 THEN ( 7 + count(s.attname) ) / 8 ELSE 0::int END
                                     + CASE WHEN bool_or(att.attname = 'oid' and att.attnum < 0) THEN 4 ELSE 0 END AS tpl_hdr_size,
                                 sum( (1-coalesce(s.null_frac, 0)) * coalesce(s.avg_width, 0) ) AS tpl_data_size,
                                 bool_or(att.atttypid = 'pg_catalog.name'::regtype)
                                     OR sum(CASE WHEN att.attnum > 0 THEN 1 ELSE 0 END) <> count(s.attname) AS is_na
                             FROM pg_attribute AS att
                                      JOIN pg_class AS tbl ON att.attrelid = tbl.oid
                                      JOIN pg_namespace AS ns ON ns.oid = tbl.relnamespace
                                      LEFT JOIN pg_stats AS s ON s.schemaname=ns.nspname AND s.tablename = tbl.relname AND s.inherited=false AND s.attname=att.attname
                                      LEFT JOIN pg_class AS toast ON tbl.reltoastrelid = toast.oid
                             WHERE NOT att.attisdropped AND tbl.relkind = 'r' AND nspname NOT IN ('pg_catalog','information_schema')
                             GROUP BY tbl.oid, ns.nspname, tbl.relname, tbl.reltuples, tbl.relpages, toast.relpages, toast.reltuples, tbl.reloptions
                         ) AS s
                ) AS s2
       ) AS s3
  WHERE NOT is_na;
$$ LANGUAGE SQL SECURITY DEFINER;

-- index bloat func
CREATE OR REPLACE FUNCTION monitor.pg_index_bloat() RETURNS TABLE(datname TEXT,nspname TEXT,relname TEXT,tblid OID,idxid OID,size BIGINT,ratio FLOAT) SET search_path = '' AS $$
    SELECT CURRENT_CATALOG AS datname, nspname, idxname AS relname, tblid, idxid,
          relpages::BIGINT * bs AS size,
          COALESCE((relpages - ( reltuples * (6 + ma - (CASE WHEN index_tuple_hdr % ma = 0 THEN ma ELSE index_tuple_hdr % ma END)
                                                  + nulldatawidth + ma - (CASE WHEN nulldatawidth % ma = 0 THEN ma ELSE nulldatawidth % ma END))
                                     / (bs - pagehdr)::FLOAT  + 1 )), 0) / relpages::FLOAT AS ratio
   FROM (
            SELECT nspname, idxname, indrelid AS tblid, indexrelid AS idxid,
                   reltuples, relpages,
                   current_setting('block_size')::INTEGER AS bs,
                   (CASE WHEN version() ~ 'mingw32' OR version() ~ '64-bit|x86_64|ppc64|ia64|amd64' THEN 8 ELSE 4 END) AS ma,
                   24 AS pagehdr,
                   (CASE WHEN max(COALESCE(pg_stats.null_frac, 0)) = 0 THEN 2 ELSE 6 END) AS index_tuple_hdr,
                   sum((1.0 - COALESCE(pg_stats.null_frac, 0.0)) *
                       COALESCE(pg_stats.avg_width, 1024))::INTEGER AS nulldatawidth
            FROM pg_attribute
                     JOIN (
                SELECT pg_namespace.nspname, ic.relname AS idxname, ic.reltuples, ic.relpages, pg_index.indrelid,
                       pg_index.indexrelid, tc.relname AS tablename,
                       regexp_split_to_table(pg_index.indkey::TEXT, ' ')::INTEGER AS attnum,
                       pg_index.indexrelid AS index_oid
                FROM pg_index
                         JOIN pg_class ic ON pg_index.indexrelid = ic.oid
                         JOIN pg_class tc ON pg_index.indrelid = tc.oid
                         JOIN pg_namespace ON ic.relnamespace = pg_namespace.oid
                         JOIN pg_am ON ic.relam = pg_am.oid
                WHERE pg_am.amname = 'btree' AND ic.relpages > 0 AND pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema')
            ) ind_atts ON pg_attribute.attrelid = ind_atts.indexrelid AND pg_attribute.attnum = ind_atts.attnum
                     JOIN pg_stats ON pg_stats.schemaname = ind_atts.nspname
                AND ((pg_stats.tablename = ind_atts.tablename AND pg_stats.attname = pg_get_indexdef(pg_attribute.attrelid, pg_attribute.attnum, TRUE))
                    OR (pg_stats.tablename = ind_atts.idxname AND pg_stats.attname = pg_attribute.attname))
            WHERE pg_attribute.attnum > 0
            GROUP BY nspname, idxname, indrelid, indexrelid, reltuples, relpages
        ) est
$$ LANGUAGE SQL SECURITY DEFINER;

CREATE OR REPLACE VIEW monitor.pg_table_bloat AS SELECT * FROM monitor.pg_table_bloat();
COMMENT ON VIEW monitor.pg_table_bloat IS 'postgres table bloat estimate';

CREATE OR REPLACE VIEW monitor.pg_index_bloat AS SELECT * FROM monitor.pg_index_bloat();
COMMENT ON VIEW monitor.pg_index_bloat IS 'postgres index bloat estimate (btree)';

GRANT EXECUTE ON FUNCTION monitor.pg_table_bloat() TO pg_monitor;
GRANT EXECUTE ON FUNCTION monitor.pg_index_bloat() TO pg_monitor;
GRANT SELECT ON monitor.pg_table_bloat TO pg_monitor;
GRANT SELECT ON monitor.pg_index_bloat TO pg_monitor;

----------------------------------------------------------------------
-- Relation Bloat : monitor.pg_bloat
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_bloat CASCADE;
CREATE OR REPLACE VIEW monitor.pg_bloat AS
SELECT coalesce(ib.datname, tb.datname)                                                   AS datname,
       coalesce(ib.nspname, tb.nspname)                                                   AS nspname,
       coalesce(ib.tblid, tb.tblid)                                                       AS tblid,
       coalesce(tb.nspname || '.' || tb.relname, ib.nspname || '.' || ib.tblid::RegClass) AS tblname,
       tb.size                                                                            AS tbl_size,
       CASE WHEN tb.ratio < 0 THEN 0 ELSE round(tb.ratio::NUMERIC, 6) END                 AS tbl_ratio,
       (tb.size * (CASE WHEN tb.ratio < 0 THEN 0 ELSE tb.ratio::NUMERIC END)) ::BIGINT    AS tbl_wasted,
       ib.idxid,
       ib.nspname || '.' || ib.relname                                                    AS idxname,
       ib.size                                                                            AS idx_size,
       CASE WHEN ib.ratio < 0 THEN 0 ELSE round(ib.ratio::NUMERIC, 5) END                 AS idx_ratio,
       (ib.size * (CASE WHEN ib.ratio < 0 THEN 0 ELSE ib.ratio::NUMERIC END)) ::BIGINT    AS idx_wasted
FROM monitor.pg_index_bloat ib
         FULL OUTER JOIN monitor.pg_table_bloat tb ON ib.tblid = tb.tblid;

COMMENT ON VIEW monitor.pg_bloat IS 'postgres relation bloat detail';
GRANT SELECT ON monitor.pg_bloat TO pg_monitor;

----------------------------------------------------------------------
-- monitor.pg_index_bloat_human
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_index_bloat_human CASCADE;
CREATE OR REPLACE VIEW monitor.pg_index_bloat_human AS
SELECT idxname                            AS name,
       tblname,
       idx_wasted                         AS wasted,
       pg_size_pretty(idx_size)           AS idx_size,
       round(100 * idx_ratio::NUMERIC, 2) AS idx_ratio,
       pg_size_pretty(idx_wasted)         AS idx_wasted,
       pg_size_pretty(tbl_size)           AS tbl_size,
       round(100 * tbl_ratio::NUMERIC, 2) AS tbl_ratio,
       pg_size_pretty(tbl_wasted)         AS tbl_wasted
FROM monitor.pg_bloat
WHERE idxname IS NOT NULL;
COMMENT ON VIEW monitor.pg_index_bloat_human IS 'postgres index bloat info in human-readable format';
GRANT SELECT ON monitor.pg_index_bloat_human TO pg_monitor;


----------------------------------------------------------------------
-- monitor.pg_table_bloat_human
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_table_bloat_human CASCADE;
CREATE OR REPLACE VIEW monitor.pg_table_bloat_human AS
SELECT tblname                                          AS name,
       idx_wasted + tbl_wasted                          AS wasted,
       pg_size_pretty(idx_wasted + tbl_wasted)          AS all_wasted,
       pg_size_pretty(tbl_wasted)                       AS tbl_wasted,
       pg_size_pretty(tbl_size)                         AS tbl_size,
       tbl_ratio,
       pg_size_pretty(idx_wasted)                       AS idx_wasted,
       pg_size_pretty(idx_size)                         AS idx_size,
       round(idx_wasted::NUMERIC * 100.0 / idx_size, 2) AS idx_ratio
FROM (SELECT datname,
             nspname,
             tblname,
             coalesce(max(tbl_wasted), 0)                         AS tbl_wasted,
             coalesce(max(tbl_size), 1)                           AS tbl_size,
             round(100 * coalesce(max(tbl_ratio), 0)::NUMERIC, 2) AS tbl_ratio,
             coalesce(sum(idx_wasted), 0)                         AS idx_wasted,
             coalesce(sum(idx_size), 1)                           AS idx_size
      FROM monitor.pg_bloat
      WHERE tblname IS NOT NULL
      GROUP BY datname,nspname,tblname
     ) d;
COMMENT ON VIEW monitor.pg_table_bloat_human IS 'postgres table bloat info in human-readable format';
GRANT SELECT ON monitor.pg_table_bloat_human TO pg_monitor;


----------------------------------------------------------------------
-- Activity Overview: monitor.pg_session
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_session CASCADE;
CREATE OR REPLACE VIEW monitor.pg_session AS
SELECT coalesce(datname, 'all') AS datname, numbackends, active, idle, ixact, max_duration, max_tx_duration, max_conn_duration
FROM (
         SELECT datname,
                count(*)                                         AS numbackends,
                count(*) FILTER ( WHERE state = 'active' )       AS active,
                count(*) FILTER ( WHERE state = 'idle' )         AS idle,
                count(*) FILTER ( WHERE state = 'idle in transaction'
                    OR state = 'idle in transaction (aborted)' ) AS ixact,
                max(extract(epoch from now() - state_change))
                FILTER ( WHERE state = 'active' )                AS max_duration,
                max(extract(epoch from now() - xact_start))      AS max_tx_duration,
                max(extract(epoch from now() - backend_start))   AS max_conn_duration
         FROM pg_stat_activity
         WHERE backend_type = 'client backend'
           AND pid <> pg_backend_pid()
         GROUP BY ROLLUP (datname)
         ORDER BY 1 NULLS FIRST
     ) t;
COMMENT ON VIEW monitor.pg_session IS 'postgres activity group by session';
GRANT SELECT ON monitor.pg_session TO pg_monitor;


----------------------------------------------------------------------
-- Sequential Scan: monitor.pg_seq_scan
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_seq_scan CASCADE;
CREATE OR REPLACE VIEW monitor.pg_seq_scan AS
    SELECT schemaname                                                        AS nspname,
           relname,
           seq_scan,
           seq_tup_read,
           seq_tup_read / seq_scan                                           AS seq_tup_avg,
           idx_scan,
           n_live_tup + n_dead_tup                                           AS tuples,
           round(n_live_tup * 100.0::NUMERIC / (n_live_tup + n_dead_tup), 2) AS live_ratio
    FROM pg_stat_user_tables
    WHERE seq_scan > 0
      and (n_live_tup + n_dead_tup) > 0
    ORDER BY seq_scan DESC;
COMMENT ON VIEW monitor.pg_seq_scan IS 'table that have seq scan';
GRANT SELECT ON monitor.pg_seq_scan TO pg_monitor;


----------------------------------------------------------------------
-- Lock Waiting: monitor.pg_lock_waiting
----------------------------------------------------------------------
DROP VIEW IF EXISTS monitor.pg_lock_waiting CASCADE;
CREATE VIEW monitor.pg_lock_waiting AS
  WITH RECURSIVE
    activity as (select pg_blocking_pids(pid) blocked_by, *,
                        extract(seconds FROM age(clock_timestamp(), xact_start)) as xact_wait,
                        extract(seconds FROM age(clock_timestamp(),
{% if pg_version|int >= 14 %}
                        (select max(l.waitstart) from pg_locks l where a.pid = l.pid)    -- PG 14 and newer
{% else %}
                        state_change       -- PG 13 and below
{% endif %}

                            )) as lock_wait
                 -- "pg_locks.waitstart" – PG14+ only; for older versions:  age(clock_timestamp(), state_change) as wait_age
                 from pg_stat_activity a
                 where state is distinct from 'idle'),
    blockers as (select array_agg(distinct c order by c) as pids
                 from (select unnest(blocked_by)
                       from activity) as dt(c)),
    tree as (select activity.*,
                    1                           as level,
                    activity.pid                as top_blocker_pid,
                    array [activity.pid]        as path,
                    array [activity.pid]::int[] as all_blockers_above
             from activity, blockers
             where array [pid] <@ blockers.pids and blocked_by = '{}'::int[]
             union all
             select activity.*,
                    tree.level + 1 as level,
                    tree.top_blocker_pid,
                    path || array [activity.pid] as path,
                    tree.all_blockers_above || array_agg(activity.pid) over () as all_blockers_above
             from activity,
                  tree
             where not array [activity.pid] <@ tree.all_blockers_above
               and activity.blocked_by <> '{}'::int[]
               and activity.blocked_by <@ tree.all_blockers_above)
  SELECT pid,
       blocked_by,
       case when wait_event_type = 'Lock' then 'waiting' else state end                                      as state,
       wait_event_type,
       wait_event,
       xact_wait,
       lock_wait,
       age(backend_xid) AS xid_age,
       -- 2147483647 - age(backend_xmin) as xmin_ttf,
       datname, usename, level - 1 AS level,
       (select count(distinct t1.pid) from tree t1 where array [tree.pid] <@ t1.path and t1.pid <> tree.pid) as blkd,
       format('%s %s%s', lpad('[' || pid::text || ']', 9, ' '), repeat('.', level - 1) || case when level > 1 then ' ' end, left(query, 1000) ) as query
FROM tree
WHERE datname = CURRENT_DATABASE()
ORDER BY top_blocker_pid, level, pid;

COMMENT ON VIEW monitor.pg_lock_waiting IS 'lock waiting tree';
GRANT SELECT ON monitor.pg_lock_waiting TO pg_monitor;



--==================================================================--
--                            Functions                             --
--==================================================================--

{% if pg_version|int >= 13 %}
----------------------------------------------------------------------
-- pg_shmem auxiliary function (PG13+ only)
----------------------------------------------------------------------
DROP FUNCTION IF EXISTS monitor.pg_shmem() CASCADE;
CREATE OR REPLACE FUNCTION monitor.pg_shmem() RETURNS SETOF pg_shmem_allocations SET search_path = '' AS
$$SELECT * FROM pg_shmem_allocations;$$ LANGUAGE SQL SECURITY DEFINER;
COMMENT ON FUNCTION monitor.pg_shmem() IS 'security wrapper for system view pg_shmem';
REVOKE ALL ON FUNCTION monitor.pg_shmem() FROM PUBLIC;
REVOKE ALL ON FUNCTION monitor.pg_shmem() FROM dbrole_readonly;
REVOKE ALL ON FUNCTION monitor.pg_shmem() FROM dbrole_offline;
GRANT EXECUTE ON FUNCTION monitor.pg_shmem() TO pg_monitor;
{% endif %}


----------------------------------------------------------------------
-- monitor.pgbouncer_auth for pgbouncer_auth_query
----------------------------------------------------------------------
{% if pgbouncer_enabled|bool %}
CREATE OR REPLACE FUNCTION monitor.pgbouncer_auth(p_username TEXT) RETURNS TABLE(username TEXT, password TEXT) SET search_path = '' AS
$$BEGIN
    RAISE WARNING 'PgBouncer auth request: %', p_username;
    RETURN QUERY SELECT rolname::TEXT, rolpassword::TEXT FROM pg_authid WHERE NOT rolsuper AND rolname = p_username;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
REVOKE ALL ON FUNCTION monitor.pgbouncer_auth(p_username TEXT) FROM PUBLIC;
REVOKE ALL ON FUNCTION monitor.pgbouncer_auth(p_username TEXT) FROM dbrole_readonly;
REVOKE ALL ON FUNCTION monitor.pgbouncer_auth(p_username TEXT) FROM dbrole_offline;
{% endif %}


--==================================================================--
--                         Foreign Tables                           --
--==================================================================--

----------------------------------------------------------------------
-- current log
----------------------------------------------------------------------
CREATE TYPE monitor.log_level AS ENUM (
    'LOG','INFO','NOTICE','WARNING','ERROR','FATAL','PANIC','DEBUG'
);
COMMENT ON TYPE monitor.log_level IS 'PostgreSQL Log Level';

-- current log
DROP FOREIGN TABLE IF EXISTS monitor.pg_log;
CREATE FOREIGN TABLE monitor.pg_log
    (
        ts        TIMESTAMPTZ, -- ts
        username  TEXT,        -- user name
        datname   TEXT,        -- database name
        pid       INTEGER,     -- process_id
        conn      TEXT,        -- connect_from
        sid       TEXT,        -- session id
        sln       BIGINT,      -- session line number
        cmd_tag   TEXT,        -- command tag
        stime     TIMESTAMPTZ, -- session start time
        vxid      TEXT,        -- virtual transaction id
        txid      BIGINT,      -- transaction id
        level     monitor.log_level, -- log level
        code      VARCHAR(5),  -- sql state error code
        msg       TEXT,        -- message
        detail    TEXT,        -- detail
        hint      TEXT,        -- hint
        iq        TEXT,        -- internal query
        iqp       INTEGER,     -- internal query position
        context   TEXT,        -- context
        q         TEXT,        -- query
        qp        INTEGER,     -- query position
        location  TEXT,        -- location
        appname   TEXT         -- application name
{% if pg_version|int >= 13%}
        ,backend    TEXT      -- backend_type (PG13)
{% endif %}
{% if pg_version|int >= 14%}
        ,leader_pid INTEGER   -- parallel group leader pid, if this is worker
{% endif %}
{% if pg_version|int >= 14%}
        ,query_id   BIGINT    -- query id of the current query
{% endif %}

) SERVER fs OPTIONS (program $$cat $(cat /pg/data/current_logfiles | awk '{print $2}');$$, format 'csv');
COMMENT ON FOREIGN TABLE monitor.pg_log IS 'current log file foreign table';

REVOKE ALL ON monitor.pg_log FROM PUBLIC;
REVOKE ALL ON monitor.pg_log FROM dbrole_offline;
REVOKE ALL ON monitor.pg_log FROM dbrole_readonly;
REVOKE ALL ON monitor.pg_log FROM dbrole_readwrite;
REVOKE ALL ON monitor.pg_log FROM dbrole_admin;
GRANT SELECT ON monitor.pg_log TO pg_monitor;


----------------------------------------------------------------------
-- pgbackrest information
----------------------------------------------------------------------
DROP FOREIGN TABLE IF EXISTS monitor.pgbackrest_info CASCADE;
CREATE FOREIGN TABLE IF NOT EXISTS monitor.pgbackrest_info (data JSONB)
    SERVER fs OPTIONS (PROGRAM $$pgbackrest --stanza={{ pg_cluster }} --output=json info$$ , FORMAT 'text');

REVOKE ALL ON monitor.pgbackrest_info FROM PUBLIC;
REVOKE ALL ON monitor.pgbackrest_info FROM dbrole_offline;
REVOKE ALL ON monitor.pgbackrest_info FROM dbrole_readonly;
REVOKE ALL ON monitor.pgbackrest_info FROM dbrole_readwrite;
GRANT SELECT ON monitor.pgbackrest_info TO pg_monitor;

DROP VIEW IF EXISTS monitor.pgbackrest;
CREATE OR REPLACE VIEW monitor.pgbackrest AS
SELECT name,
       value ->> 'type'                                          AS bk_type,
       (value ->> 'error')::BOOLEAN                              AS bk_error,
       current_archive ->> 'min'                                 as wal_min,
       current_archive ->> 'max'                                 as wal_max,
       value ->> 'label'                                         AS bk_label,
       value ->> 'prior'                                         AS bk_prior,
       (value -> 'timestamp' ->> 'start')::NUMERIC               AS bk_start_ts,
       (value -> 'timestamp' ->> 'stop')::NUMERIC                AS bk_stop_ts,
       to_timestamp((value -> 'timestamp' ->> 'start')::NUMERIC) AS bk_start_at,
       to_timestamp((value -> 'timestamp' ->> 'stop')::NUMERIC)  AS bk_stop_at,
       value -> 'lsn' ->> 'start'                                AS bk_start_lsn,
       value -> 'lsn' ->> 'stop'                                 AS bk_stop_lsn,
       (value -> 'info' ->> 'size')::BIGINT                      AS bk_size,
       (value -> 'info' ->> 'delta')::BIGINT                     AS bk_delta,
       (value -> 'info' -> 'repo' ->> 'size')::BIGINT            AS bk_repo_size,
       (value -> 'info' -> 'repo' ->> 'delta')::BIGINT           AS bk_repo_delta,
       value -> 'reference'                                      AS bk_reference,
       value -> 'annotation'                                     AS bk_annotation
FROM (SELECT value ->> 'name'   AS name,
             value -> 'backup'  AS backups,
             value -> 'archive' -> (jsonb_array_length(value -> 'archive') - 1) AS current_archive
      FROM monitor.pgbackrest_info i, jsonb_array_elements(i.data)) z, jsonb_array_elements(z.backups);

REVOKE ALL ON monitor.pgbackrest FROM PUBLIC;
REVOKE ALL ON monitor.pgbackrest FROM dbrole_offline;
REVOKE ALL ON monitor.pgbackrest FROM dbrole_readonly;
REVOKE ALL ON monitor.pgbackrest FROM dbrole_readwrite;
REVOKE ALL ON monitor.pgbackrest FROM dbrole_admin;
GRANT SELECT ON monitor.pgbackrest TO pg_monitor;


----------------------------------------------------------------------
-- patroni information
----------------------------------------------------------------------
DROP FOREIGN TABLE IF EXISTS monitor.patroni_info CASCADE;
CREATE FOREIGN TABLE IF NOT EXISTS monitor.patroni_info (data JSONB)
    SERVER fs OPTIONS (PROGRAM $$curl http{% if patroni_ssl_enabled|bool %}s{% endif %}://127.0.0.1:{{ patroni_port }}/cluster$$ , FORMAT 'text');

REVOKE ALL ON monitor.patroni_info FROM PUBLIC;
REVOKE ALL ON monitor.patroni_info FROM dbrole_offline;
REVOKE ALL ON monitor.patroni_info FROM dbrole_readonly;
REVOKE ALL ON monitor.patroni_info FROM dbrole_readwrite;
GRANT SELECT ON monitor.patroni_info TO pg_monitor;

DROP FOREIGN TABLE IF EXISTS monitor.patroni_conf CASCADE;
CREATE FOREIGN TABLE IF NOT EXISTS monitor.patroni_conf (data JSONB)
    SERVER fs OPTIONS (PROGRAM $$cat patroni.dynamic.json$$ , FORMAT 'text');

REVOKE ALL ON monitor.patroni_conf FROM PUBLIC;
REVOKE ALL ON monitor.patroni_conf FROM dbrole_offline;
REVOKE ALL ON monitor.patroni_conf FROM dbrole_readonly;
REVOKE ALL ON monitor.patroni_conf FROM dbrole_readwrite;
GRANT SELECT ON monitor.patroni_conf TO pg_monitor;

DROP VIEW IF EXISTS monitor.patroni;
CREATE OR REPLACE VIEW monitor.patroni AS
SELECT value ->> 'name'                                                             AS name,
       CASE value ->> 'role' WHEN 'leader' THEN 'primary' ELSE value ->> 'role' END AS role,
       value ->> 'host'                                                             AS host,
       value ->> 'port'                                                             AS port,
       value ->> 'state'                                                            AS state,
       (value ->> 'timeline')::INTEGER                                              AS timeline,
       (value ->> 'lag')::BIGINT                                                    AS lag,
       value ->> 'api_url'                                                          AS url,
       value -> 'tags' ->> 'replicatefrom'                                          AS replicatefrom,
       coalesce((value -> 'tags' -> 'nofailover') ::BOOLEAN, false)::BOOLEAN        AS nofailover,
       coalesce((value -> 'tags' -> 'clonefrom') ::BOOLEAN, false)::BOOLEAN         AS clonefrom,
       coalesce((value -> 'tags' -> 'noloadbalance') ::BOOLEAN, false)::BOOLEAN     AS noloadbalance,
       coalesce((value -> 'tags' -> 'nosync') ::BOOLEAN, false)::BOOLEAN            AS nosync,
       value -> 'tags'                                                              AS tags
FROM monitor.patroni_info i, jsonb_array_elements(data -> 'members');

REVOKE ALL ON monitor.patroni FROM PUBLIC;
REVOKE ALL ON monitor.patroni FROM dbrole_offline;
REVOKE ALL ON monitor.patroni FROM dbrole_readonly;
REVOKE ALL ON monitor.patroni FROM dbrole_readwrite;
GRANT SELECT ON monitor.patroni TO pg_monitor;

-- get process status
DROP FOREIGN TABLE IF EXISTS monitor.process_status CASCADE;
CREATE FOREIGN TABLE monitor.process_status (
  username TEXT,
  pid      INTEGER,
  cpu      NUMERIC,
  mem      NUMERIC,
  vsz      BIGINT,
  rss      BIGINT,
  tty      TEXT,
  stat     TEXT,
  start    TEXT,
  uptime   TEXT,
  command  TEXT
) SERVER fs OPTIONS (PROGRAM $$
ps aux | awk '{print $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,substr($0,index($0,$11))}' OFS='\037'
$$ , FORMAT 'csv', DELIMITER E'\037', HEADER 'TRUE');

REVOKE ALL ON monitor.process_status FROM PUBLIC;
REVOKE ALL ON monitor.process_status FROM dbrole_offline;
REVOKE ALL ON monitor.process_status FROM dbrole_readonly;
REVOKE ALL ON monitor.process_status FROM dbrole_readwrite;
GRANT SELECT ON monitor.process_status TO pg_monitor;


-- get disk usage
DROP FOREIGN TABLE IF EXISTS monitor.disk_free CASCADE;
CREATE FOREIGN TABLE monitor.disk_free (
  fsname      TEXT,
  fstype      TEXT,
  total_1m    BIGINT,
  used_1m     BIGINT,
  avail_1m    BIGINT,
  percent     TEXT,
  itotal      BIGINT,
  iused       BIGINT,
  ifree       BIGINT,
  ipercent    TEXT,
  mountpoint  TEXT
) SERVER fs OPTIONS (PROGRAM $$
df -ml --output=source,fstype,size,used,avail,pcent,itotal,iused,iavail,ipcent,target | tail -n +2 | awk '{print $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11}' OFS='\037'
$$,
FORMAT 'csv', DELIMITER E'\037'
);

REVOKE ALL ON monitor.disk_free FROM PUBLIC;
REVOKE ALL ON monitor.disk_free FROM dbrole_offline;
REVOKE ALL ON monitor.disk_free FROM dbrole_readonly;
REVOKE ALL ON monitor.disk_free FROM dbrole_readwrite;
GRANT SELECT ON monitor.disk_free TO pg_monitor;

--==================================================================--
--                          Customize Logic                         --
--==================================================================--
-- This script will be executed on primary instance among a newly created
-- postgres cluster. it will be executed as dbsu on template1 database
-- add your customization logic here (make sure they are idempotent)
