-- This program is open source, licensed under the PostgreSQL License.
-- For license terms, see the LICENSE file.
--
-- Copyright (C) 2015-2022: Julien Rouhaud

-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION pg_track_settings" to load this file. \quit

SET client_encoding = 'UTF8';

CREATE UNLOGGED TABLE public.pg_track_settings_settings_src_tmp (
    srvid integer NOT NULL,
    ts timestamp with time zone NOT NULL,
    name    text NOT NULL,
    setting text,
    current_setting text
);
-- no need to backup this table

CREATE TABLE public.pg_track_settings_list (
    srvid integer NOT NULL,
    name text,
    PRIMARY KEY (srvid, name)
);
SELECT pg_catalog.pg_extension_config_dump('public.pg_track_settings_list', '');

CREATE TABLE public.pg_track_settings_history (
    srvid integer NOT NULL,
    ts timestamp with time zone,
    name text NOT NULL,
    setting text,
    is_dropped boolean NOT NULL DEFAULT false,
    setting_pretty text,
    PRIMARY KEY(srvid, ts, name)
);
SELECT pg_catalog.pg_extension_config_dump('public.pg_track_settings_history', '');

CREATE UNLOGGED TABLE public.pg_track_settings_rds_src_tmp (
    srvid integer NOT NULL,
    ts timestamp with time zone NOT NULL,
    name text NOT NULL,
    setting text,
    setdatabase oid NOT NULL,
    setrole oid NOT NULL
);
-- no need to backup this table

CREATE TABLE public.pg_track_db_role_settings_list (
    srvid integer,
    name text,
    setdatabase oid,
    setrole oid,
    PRIMARY KEY (srvid, name, setdatabase, setrole)
);
SELECT pg_catalog.pg_extension_config_dump('public.pg_track_db_role_settings_list', '');

CREATE TABLE public.pg_track_db_role_settings_history (
    srvid INTEGER NOT NULL,
    ts timestamp with time zone,
    name text,
    setdatabase oid,
    setrole oid,
    setting text,
    is_dropped boolean NOT NULL DEFAULT false,
    PRIMARY KEY(srvid, ts, name, setdatabase, setrole)
);
SELECT pg_catalog.pg_extension_config_dump('public.pg_track_db_role_settings_history', '');

CREATE UNLOGGED TABLE public.pg_track_settings_reboot_src_tmp (
    srvid integer NOT NULL,
    ts timestamp with time zone NOT NULL,
    postmaster_ts timestamp with time zone NOT NULL
);
-- no need to backup this table

CREATE TABLE public.pg_reboot (
    srvid integer NOT NULL,
    ts timestamp with time zone,
    PRIMARY KEY (srvid, ts)
);
SELECT pg_catalog.pg_extension_config_dump('public.pg_reboot', '');

----------------------
-- source functions --
----------------------
CREATE OR REPLACE FUNCTION pg_track_settings_settings_src (
    IN _srvid integer,
    OUT ts timestamp with time zone,
    OUT name text,
    OUT setting text,
    OUT current_setting text
)
RETURNS SETOF record AS $PROC$
BEGIN
    IF (_srvid = 0) THEN
        RETURN QUERY SELECT now(),
            s.name, s.setting, pg_catalog.current_setting(s.name)
        FROM pg_catalog.pg_settings s;
    ELSE
        RETURN QUERY SELECT s.ts,
            s.name, s.setting, s.current_setting
        FROM public.pg_track_settings_settings_src_tmp s;
    END IF;
END;
$PROC$ LANGUAGE plpgsql; /* end of pg_track_settings_settings_src */

CREATE OR REPLACE FUNCTION pg_track_settings_rds_src (
    IN _srvid integer,
    OUT ts timestamp with time zone,
    OUT name text,
    OUT setting text,
    OUT setdatabase oid,
    OUT setrole oid
)
RETURNS SETOF record AS $PROC$
BEGIN
    IF (_srvid = 0) THEN
        RETURN QUERY SELECT now(),
            (regexp_split_to_array(unnest(s.setconfig),'=')::text[])[1] AS name,
            (regexp_split_to_array(unnest(s.setconfig),'=')::text[])[2] AS setting,
            s.setdatabase, s.setrole
        FROM pg_catalog.pg_db_role_setting s;
    ELSE
        RETURN QUERY SELECT s.ts,
            s.name, s.setting, s.setdatabase, s.setrole
        FROM public.pg_track_settings_rds_src_tmp s;
    END IF;
END;
$PROC$ LANGUAGE plpgsql; /* end of pg_track_settings_rds_src */

CREATE OR REPLACE FUNCTION pg_track_settings_reboot_src (
    IN _srvid integer,
    OUT ts timestamp with time zone,
    OUT postmaster_ts timestamp with time zone
)
RETURNS SETOF record AS $PROC$
BEGIN
    IF (_srvid = 0) THEN
        RETURN QUERY SELECT now(),
            pg_postmaster_start_time();
    ELSE
        RETURN QUERY SELECT s.ts,
            s.postmaster_ts
        FROM public.pg_track_settings_reboot_src_tmp s;
    END IF;
END;
$PROC$ LANGUAGE plpgsql; /* end of pg_track_settings_reboot_src */

------------------------
-- snapshot functions --
------------------------
CREATE OR REPLACE FUNCTION pg_track_settings_snapshot_settings(_srvid integer)
    RETURNS boolean AS
$_$
DECLARE
    _snap_ts timestamp with time zone = NULL;
BEGIN
    SELECT max(ts) INTO _snap_ts
    FROM public.pg_track_settings_settings_src(_srvid);

    -- this function should have been called for previously saved data.  If
    -- not, probably somethig went wrong, so discard those data
    IF (_srvid != 0) THEN
        DELETE FROM public.pg_track_settings_settings_src_tmp
        WHERE ts != _snap_ts;
    END IF;

    -- Handle dropped GUC
    WITH src AS (
        SELECT * FROM public.pg_track_settings_settings_src(_srvid)
    ),
    dropped AS (
        SELECT s.ts, l.srvid, l.name
        FROM public.pg_track_settings_list l
        LEFT JOIN src s ON s.name = l.name
        WHERE l.srvid = _srvid
          AND s.name IS NULL
    ),
    mark_dropped AS (
        INSERT INTO public.pg_track_settings_history (srvid, ts, name, setting,
            setting_pretty, is_dropped)
        SELECT srvid, COALESCE(_snap_ts, now()), name, NULL, NULL, true
        FROM dropped
    )
    DELETE FROM public.pg_track_settings_list l
    USING dropped d
    WHERE d.name = l.name
      AND d.srvid = l.srvid
      AND l.srvid = _srvid;

    -- Insert missing settings
    INSERT INTO public.pg_track_settings_list (srvid, name)
    SELECT _srvid, name
    FROM public.pg_track_settings_settings_src(_srvid) s
    WHERE NOT EXISTS (SELECT 1
        FROM public.pg_track_settings_list l
        WHERE l.srvid = _srvid
          AND l.name = s.name
    );

    -- Detect changed GUC, insert new vals
    WITH src AS (
        SELECT * FROM public.pg_track_settings_settings_src(_srvid)
    ), last_snapshot AS (
        SELECT srvid, name, setting
        FROM (
            SELECT srvid, name, setting,
              row_number() OVER (PARTITION BY NAME ORDER BY ts DESC) AS rownum
            FROM public.pg_track_settings_history h
            WHERE h.srvid = _srvid
        ) all_snapshots
        WHERE rownum = 1
    )
    INSERT INTO public.pg_track_settings_history
      (srvid, ts, name, setting, setting_pretty)
    SELECT _srvid, s.ts, s.name, s.setting, s.current_setting
    FROM src s
    LEFT JOIN last_snapshot l ON l.name = s.name
    WHERE (
        l.name IS NULL
        OR l.setting IS DISTINCT FROM s.setting
    );

    IF (_srvid != 0) THEN
        DELETE FROM public.pg_track_settings_settings_src_tmp
        WHERE srvid = _srvid;
    END IF;

    RETURN true;
END;
$_$
LANGUAGE plpgsql; /* end of pg_track_settings_snapshot_settings() */

CREATE OR REPLACE FUNCTION pg_track_settings_snapshot_rds(_srvid integer)
    RETURNS boolean AS
$_$
DECLARE
    _snap_ts timestamp with time zone;
BEGIN
    SELECT max(ts) INTO _snap_ts
    FROM public.pg_track_settings_rds_src(_srvid);

    -- this function should have been called for previously saved data.  If
    -- not, probably somethig went wrong, so discard those data
    IF (_srvid != 0) THEN
        DELETE FROM public.pg_track_settings_rds_src_tmp
        WHERE ts != _snap_ts;
    END IF;

    -- Handle dropped db_role_setting
    WITH rds AS (
        SELECT * FROM public.pg_track_settings_rds_src(_srvid)
    ),
    dropped AS (
        SELECT s.ts, l.setdatabase, l.setrole, l.name
        FROM public.pg_track_db_role_settings_list l
        LEFT JOIN rds s ON (
            s.setdatabase = l.setdatabase
            AND s.setrole = l.setrole
            AND s.name = l.name
        )
        WHERE l.srvid = _srvid
            AND s.setdatabase IS NULL
            AND s.setrole IS NULL
            AND s.name IS NULL
    ),
    mark_dropped AS (
        INSERT INTO public.pg_track_db_role_settings_history
            (srvid, ts, setdatabase, setrole, name, setting, is_dropped)
        SELECT _srvid, ts, d.setdatabase, d.setrole, d.name, NULL, true
        FROM dropped AS d
    )
    DELETE FROM public.pg_track_db_role_settings_list l
    USING dropped d
    WHERE
        l.srvid = _srvid
        AND d.setdatabase = l.setdatabase
        AND d.setrole = l.setrole
        AND d.name = l.name;

    -- Insert missing settings
    WITH rds AS (
        SELECT * FROM public.pg_track_settings_rds_src(_srvid)
    )
    INSERT INTO public.pg_track_db_role_settings_list
        (srvid, setdatabase, setrole, name)
    SELECT _srvid, setdatabase, setrole, name
    FROM rds s
    WHERE NOT EXISTS (SELECT 1
        FROM public.pg_track_db_role_settings_list l
        WHERE
            l.srvid = _srvid
            AND l.setdatabase = s.setdatabase
            AND l.setrole = l.setrole
            AND l.name = s.name
    );

    -- Detect changed GUC, insert new vals
    WITH rds AS (
        SELECT * FROM public.pg_track_settings_rds_src(_srvid)
    ),
    last_snapshot AS (
        SELECT setdatabase, setrole, name, setting
        FROM (
            SELECT setdatabase, setrole, name, setting,
                row_number() OVER (PARTITION BY name, setdatabase, setrole ORDER BY ts DESC) AS rownum
            FROM public.pg_track_db_role_settings_history
            WHERE srvid = _srvid
        ) all_snapshots
        WHERE rownum = 1
    )
    INSERT INTO public.pg_track_db_role_settings_history
        (srvid, ts, setdatabase, setrole, name, setting)
    SELECT _srvid, s.ts, s.setdatabase, s.setrole, s.name, s.setting
    FROM rds s
    LEFT JOIN last_snapshot l ON
        l.setdatabase = s.setdatabase
        AND l.setrole = s.setrole
        AND l.name = s.name
    WHERE (l.setdatabase IS NULL
        AND l.setrole IS NULL
        AND l.name IS NULL)
    OR (l.setting IS DISTINCT FROM s.setting);

    IF (_srvid != 0) THEN
        DELETE FROM public.pg_track_settings_rds_src_tmp
        WHERE srvid = _srvid;
    END IF;

    RETURN true;
END;
$_$
LANGUAGE plpgsql; /* end of pg_track_settings_snapshot_rds() */

CREATE OR REPLACE FUNCTION public.pg_track_settings_snapshot_reboot(_srvid integer)
    RETURNS boolean AS
$_$
BEGIN
    -- Detect is postmaster restarted since last call
    WITH last_reboot AS (
        SELECT t.postmaster_ts
        FROM public.pg_track_settings_reboot_src(_srvid) t
    )
    INSERT INTO public.pg_reboot (srvid, ts)
    SELECT _srvid, lr.postmaster_ts FROM last_reboot lr
    WHERE NOT EXISTS (SELECT 1
        FROM public.pg_reboot r
        WHERE r.srvid = _srvid
        AND r.ts = lr.postmaster_ts
        AND r.srvid = _srvid
    );

    IF (_srvid != 0) THEN
        DELETE FROM public.pg_track_settings_reboot_src_tmp
        WHERE srvid = _srvid;
    END IF;

    RETURN true;
END;
$_$
LANGUAGE plpgsql; /* end of pg_track_settings_snapshot_reboot() */

-- global function doing all the work for local instance, kept for backward
-- compatibility
CREATE OR REPLACE FUNCTION pg_track_settings_snapshot()
RETURNS boolean AS
$_$
BEGIN
    PERFORM public.pg_track_settings_snapshot_settings(0);
    PERFORM public.pg_track_settings_snapshot_rds(0);
    PERFORM public.pg_track_settings_snapshot_reboot(0);

    RETURN true;
END;
$_$
LANGUAGE plpgsql;
/* end of pg_track_settings_snapshot() */

CREATE OR REPLACE FUNCTION public.pg_track_settings(
    _ts timestamp with time zone DEFAULT now(),
    _srvid integer DEFAULT 0)
RETURNS TABLE (name text, setting text, setting_pretty text) AS
$_$
BEGIN
    RETURN QUERY
        SELECT s.name, s.setting, s.setting_pretty
        FROM (
            SELECT h.name, h.setting, h.setting_pretty, h.is_dropped,
            row_number() OVER (PARTITION BY h.name ORDER BY h.ts DESC) AS rownum
            FROM public.pg_track_settings_history h
            WHERE h.srvid = _srvid
            AND h.ts <= _ts
        ) s
        WHERE s.rownum = 1
        AND NOT s.is_dropped
        ORDER BY s.name;
END;
$_$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION public.pg_track_db_role_settings(
    _ts timestamp with time zone DEFAULT now(),
    _srvid integer DEFAULT 0)
RETURNS TABLE (setdatabase oid, setrole oid, name text, setting text) AS
$_$
BEGIN
    RETURN QUERY
        SELECT s.setdatabase, s.setrole, s.name, s.setting
        FROM (
            SELECT h.setdatabase, h.setrole, h.name, h.setting, h.is_dropped,
                row_number() OVER (PARTITION BY h.name, h.setdatabase, h.setrole ORDER BY h.ts DESC) AS rownum
            FROM public.pg_track_db_role_settings_history h
            WHERE h.srvid = _srvid
            AND h.ts <= _ts
        ) s
        WHERE s.rownum = 1
        AND NOT s.is_dropped
        ORDER BY s.setdatabase, s.setrole, s.name;
END;
$_$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION public.pg_track_settings_diff(
    _from timestamp with time zone,
    _to timestamp with time zone,
    _srvid integer DEFAULT 0)
RETURNS TABLE (name text, from_setting text, from_exists boolean,
    to_setting text, to_exists boolean,
    from_setting_pretty text, to_setting_pretty text) AS
$_$
BEGIN
    RETURN QUERY
        SELECT COALESCE(s1.name, s2.name),
               s1.setting AS from_setting,
               CASE WHEN s1.setting IS NULL THEN false ELSE true END,
               s2.setting AS to_setting,
               CASE WHEN s2.setting IS NULL THEN false ELSE true END,
               s1.setting_pretty AS from_setting_pretty,
               s2.setting_pretty AS to_setting_pretty
        FROM public.pg_track_settings(_from, _srvid) s1
        FULL OUTER JOIN public.pg_track_settings(_to, _srvid) s2 ON s2.name = s1.name
        WHERE s1.setting IS DISTINCT FROM s2.setting
        ORDER BY 1;
END;
$_$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION public.pg_track_db_role_settings_diff(
    _from timestamp with time zone,
    _to timestamp with time zone,
    _srvid integer DEFAULT 0)
RETURNS TABLE (setdatabase oid, setrole oid, name text,
    from_setting text, from_exists boolean, to_setting text, to_exists boolean)
AS
$_$
BEGIN
    RETURN QUERY
        SELECT COALESCE(s1.setdatabase, s2.setdatabase),
               COALESCE(s1.setrole, s2.setrole),
               COALESCE(s1.name, s2.name),
               s1.setting AS from_setting,
               CASE WHEN s1.setting IS NULL THEN false ELSE true END,
               s2.setting AS to_setting,
               CASE WHEN s2.setting IS NULL THEN false ELSE true END
        FROM public.pg_track_db_role_settings(_from, _srvid) s1
        FULL OUTER JOIN public.pg_track_db_role_settings(_to, _srvid) s2 ON
            s2.setdatabase = s1.setdatabase
            AND s2.setrole = s1.setrole
            AND s2.name = s1.name
        WHERE
            s1.setdatabase IS DISTINCT FROM s2.setdatabase
            AND s1.setrole IS DISTINCT FROM s2.setrole
            AND s1.setting IS DISTINCT FROM s2.setting
        ORDER BY 1, 2, 3;
END;
$_$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION pg_track_settings_log(
    _name text,
    _srvid integer DEFAULT 0)
RETURNS TABLE (ts timestamp with time zone, name text, setting_exists boolean,
    setting text, setting_pretty text) AS
$_$
BEGIN
    RETURN QUERY
        SELECT h.ts, h.name, NOT h.is_dropped, h.setting, h.setting_pretty
        FROM public.pg_track_settings_history h
        WHERE h.srvid = _srvid
        AND h.name = _name
        ORDER BY ts DESC;
END;
$_$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION pg_track_db_role_settings_log(
    _name text,
    _srvid integer DEFAULT 0)
RETURNS TABLE (ts timestamp with time zone, setdatabase oid, setrole oid,
    name text, setting_exists boolean, setting text) AS
$_$
BEGIN
    RETURN QUERY
        SELECT h.ts, h.setdatabase, h.setrole, h.name, NOT h.is_dropped, h.setting
        FROM public.pg_track_db_role_settings_history h
        WHERE h.srvid = _srvid
        AND h.name = _name
        ORDER BY ts, setdatabase, setrole DESC;
END;
$_$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION pg_track_reboot_log(_srvid integer DEFAULT 0)
RETURNS TABLE (ts timestamp with time zone) AS
$_$
BEGIN
    RETURN QUERY
        SELECT r.ts
        FROM public.pg_reboot r
        WHERE r.srvid = _srvid
        ORDER BY r.ts;
END;
$_$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION public.pg_track_settings_reset(_srvid integer DEFAULT 0)
RETURNS void AS
$_$
BEGIN
    DELETE FROM public.pg_track_settings_settings_src_tmp WHERE srvid = _srvid;
    DELETE FROM public.pg_track_settings_rds_src_tmp WHERE srvid = _srvid;
    DELETE FROM public.pg_track_settings_reboot_src_tmp WHERE srvid = _srvid;
    DELETE FROM public.pg_track_settings_list WHERE srvid = _srvid;
    DELETE FROM public.pg_track_settings_history WHERE srvid = _srvid;
    DELETE FROM public.pg_track_db_role_settings_list WHERE srvid = _srvid;
    DELETE FROM public.pg_track_db_role_settings_history WHERE srvid = _srvid;
    DELETE FROM public.pg_reboot WHERE srvid = _srvid;
END;
$_$
LANGUAGE plpgsql;
