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

DO $$
DECLARE
    v_typname text;
    v_suffix text;
    v_coltype text;
BEGIN
    FOREACH v_typname IN ARRAY ARRAY[
        'powa_stat_subscription_stats_history_record',
        'powa_stat_subscription_stats_history_db_record',
        'powa_stat_subscription_stats_history_diff',
        'powa_stat_subscription_stats_history_rate'
    ] LOOP
        IF v_typname = 'powa_stat_subscription_stats_history_rate' THEN
            v_suffix := '_per_sec';
            v_coltype = 'double precision';
        ELSE
            v_suffix := '';
            v_coltype = 'bigint';
        END IF;

        EXECUTE format('ALTER TYPE @extschema@.%I
            -- 19+
            RENAME ATTRIBUTE sync_error_count%2$s TO sync_table_error_count%2$s',
            v_typname, v_suffix);
        EXECUTE format('ALTER TYPE @extschema@.%I
            -- 19+
            ADD ATTRIBUTE sync_seq_error_count%2$s %3$s,
            -- pg18+
            ADD ATTRIBUTE confl_insert_exists%2$s %3$s,
            ADD ATTRIBUTE confl_update_origin_differs%2$s %3$s,
            ADD ATTRIBUTE confl_update_exists%2$s %3$s,
            -- pg19+
            ADD ATTRIBUTE confl_update_deleted%2$s %3$s,
            -- pg18+
            ADD ATTRIBUTE confl_update_missing%2$s %3$s,
            ADD ATTRIBUTE confl_delete_origin_differs%2$s %3$s,
            ADD ATTRIBUTE confl_delete_missing%2$s %3$s,
            ADD ATTRIBUTE confl_multiple_unique_conflicts%2$s %3$s',
            v_typname, v_suffix, v_coltype);
    END LOOP;
END;
$$ LANGUAGE plpgsql;

ALTER TABLE @extschema@.powa_stat_subscription_stats_src_tmp
    -- 19+
    RENAME COLUMN sync_error_count TO sync_table_error_count;
ALTER TABLE @extschema@.powa_stat_subscription_stats_src_tmp
    -- pg19+
    ADD COLUMN sync_seq_error_count bigint NOT NULL,
    -- pg18+
    ADD COLUMN confl_insert_exists bigint NOT NULL,
    ADD COLUMN confl_update_origin_differs bigint NOT NULL,
    ADD COLUMN confl_update_exists bigint NOT NULL,
    -- pg19+
    ADD COLUMN confl_update_deleted bigint NOT NULL,
    -- pg18+
    ADD COLUMN confl_update_missing bigint NOT NULL,
    ADD COLUMN confl_delete_origin_differs bigint NOT NULL,
    ADD COLUMN confl_delete_missing bigint NOT NULL,
    ADD COLUMN confl_multiple_unique_conflicts bigint NOT NULL;

DO $$
BEGIN
    IF current_setting('server_version_num')::bigint >= 180000 THEN
        ALTER TABLE @extschema@.powa_stat_subscription_stats_src_tmp
            RENAME CONSTRAINT powa_stat_subscription_stats_src_tmp_sync_error_count_not_null
                TO powa_stat_subscription_stats_sr_sync_table_error_count_not_null;
    END IF;
END;
$$ LANGUAGE plpgsql;

DROP FUNCTION @extschema@.powa_stat_subscription_stats_src(integer);
CREATE FUNCTION @extschema@.powa_stat_subscription_stats_src(IN _srvid integer,
    OUT ts timestamp with time zone,
    OUT subid oid,
    OUT apply_error_count bigint,
    OUT sync_table_error_count bigint,
    OUT stats_reset timestamp with time zone,
    OUT sync_seq_error_count bigint,
    OUT confl_insert_exists bigint,
    OUT confl_update_origin_differs bigint,
    OUT confl_update_exists bigint,
    OUT confl_update_deleted bigint,
    OUT confl_update_missing bigint,
    OUT confl_delete_origin_differs bigint,
    OUT confl_delete_missing bigint,
    OUT confl_multiple_unique_conflicts bigint
) RETURNS SETOF record STABLE AS $PROC$
DECLARE
    v_pg_version_num int;
BEGIN
    IF (_srvid = 0) THEN
        v_pg_version_num := current_setting('server_version_num')::int;

        -- pg19+:
        -- sync_error_count renamed to sync_table_error_count,
        -- sync_seq_error_count added
        -- confl_update_deleted added
        IF v_pg_version_num >= 190000 THEN
            RETURN QUERY SELECT now(),
            s.subid,
            s.apply_error_count, s.sync_table_error_count,
            s.stats_reset,
            s.sync_seq_error_count,
            s.confl_insert_exists,
            s.confl_update_origin_differs,
            s.confl_update_exists,
            s.confl_update_deleted,
            s.confl_update_missing,
            s.confl_delete_origin_differs,
            s.confl_delete_missing,
            s.confl_multiple_unique_conflicts
            FROM pg_catalog.pg_stat_subscription_stats AS s;
        -- pg18+, confl_* columns added
        ELSIF v_pg_version_num >= 180000 THEN
            RETURN QUERY SELECT now(),
            s.subid,
            s.apply_error_count, s.sync_error_count AS sync_table_error_count,
            s.stats_reset,
            0::bigint AS sync_seq_error_count,
            s.confl_insert_exists,
            s.confl_update_origin_differs,
            s.confl_update_exists,
            0::bigint AS confl_update_deleted,
            s.confl_update_missing,
            s.confl_delete_origin_differs,
            s.confl_delete_missing,
            s.confl_multiple_unique_conflicts
            FROM pg_catalog.pg_stat_subscription_stats AS s;
        -- pg15+, the view is introduced
        ELSIF v_pg_version_num >= 150000 THEN
            RETURN QUERY SELECT now(),
            s.subid,
            s.apply_error_count, s.sync_error_count AS sync_table_error_count,
            s.stats_reset,
            0::bigint AS sync_seq_error_count,
            0::bigint AS confl_insert_exists,
            0::bigint AS confl_update_origin_differs,
            0::bigint AS confl_update_exists,
            0::bigint AS confl_update_deleted,
            0::bigint AS confl_update_missing,
            0::bigint AS confl_delete_origin_differs,
            0::bigint AS confl_delete_missing,
            0::bigint AS confl_multiple_unique_conflicts
            FROM pg_catalog.pg_stat_subscription_stats AS s;
        ELSE -- return an empty dataset for pg9.6- servers
            RETURN QUERY SELECT now(),
            0::oid AS subid,
            0::bigint AS apply_error_count, 0::bigint AS sync_table_error_count,
            NULL::timestamp with time zone AS stats_reset,
            0::bigint AS sync_seq_error_count,
            0::bigint AS confl_insert_exists,
            0::bigint AS confl_update_origin_differs,
            0::bigint AS confl_update_exists,
            0::bigint AS confl_update_deleted,
            0::bigint AS confl_update_missing,
            0::bigint AS confl_delete_origin_differs,
            0::bigint AS confl_delete_missing,
            0::bigint AS confl_multiple_unique_conflicts
            WHERE false;
        END IF;
    ELSE
        RETURN QUERY SELECT s.ts,
            s.subid,
            s.apply_error_count, s.sync_table_error_count,
            s.stats_reset,
            s.sync_seq_error_count,
            s.confl_insert_exists,
            s.confl_update_origin_differs,
            s.confl_update_exists,
            s.confl_update_deleted,
            s.confl_update_missing,
            s.confl_delete_origin_differs,
            s.confl_delete_missing,
            s.confl_multiple_unique_conflicts
        FROM @extschema@.powa_stat_subscription_stats_src_tmp AS s
        WHERE s.srvid = _srvid;
    END IF;
END;
$PROC$ LANGUAGE plpgsql; /* end of powa_stat_subscription_stats_src */

CREATE OR REPLACE FUNCTION @extschema@.powa_stat_subscription_stats_snapshot(_srvid integer)
 RETURNS void
AS $PROC$
DECLARE
    result boolean;
    v_funcname    text := format('@extschema@.%I(%s)',
                                 'powa_stat_subscription_stats_snapshot', _srvid);
    v_rowcount    bigint;
BEGIN
    PERFORM @extschema@.powa_log(format('running %s', v_funcname));

    PERFORM @extschema@.powa_prevent_concurrent_snapshot(_srvid);

    -- Insert background writer statistics
    WITH rel AS (
        SELECT *
        FROM @extschema@.powa_stat_subscription_stats_src(_srvid)
    )
    INSERT INTO @extschema@.powa_stat_subscription_stats_history_current
        SELECT _srvid, subid,
        ROW(ts, apply_error_count, sync_table_error_count, stats_reset,
            sync_seq_error_count, confl_insert_exists, confl_update_origin_differs,
            confl_update_exists, confl_update_deleted, confl_update_missing,
            confl_delete_origin_differs, confl_delete_missing, confl_multiple_unique_conflicts)::@extschema@.powa_stat_subscription_stats_history_record AS record
        FROM rel;

    GET DIAGNOSTICS v_rowcount = ROW_COUNT;
    PERFORM @extschema@.powa_log(format('%s - rowcount: %s',
            v_funcname, v_rowcount));

    IF (_srvid != 0) THEN
        DELETE FROM @extschema@.powa_stat_subscription_stats_src_tmp WHERE srvid = _srvid;
    END IF;

    result := true;
END;
$PROC$ LANGUAGE plpgsql; /* end of powa_stat_subscription_stats_snapshot */

CREATE OR REPLACE FUNCTION @extschema@.powa_stat_subscription_stats_aggregate(_srvid integer)
 RETURNS void
AS $PROC$
DECLARE
    v_funcname    text := format('@extschema@.%I(%s)',
                                 'powa_stat_subscription_stats_aggregate', _srvid);
    v_rowcount    bigint;
BEGIN
    PERFORM @extschema@.powa_log(format('running %s', v_funcname));

    PERFORM @extschema@.powa_prevent_concurrent_snapshot(_srvid);

    -- aggregate powa_stat_subscription_stats history table
    INSERT INTO @extschema@.powa_stat_subscription_stats_history
        SELECT srvid, subid,
            tstzrange(min((record).ts), max((record).ts),'[]'),
            array_agg(record),
            ROW(min((record).ts),
                    min((record).apply_error_count),
                    min((record).sync_table_error_count),
                    min((record).stats_reset),
                    min((record).sync_seq_error_count),
                    min((record).confl_insert_exists),
                    min((record).confl_update_origin_differs),
                    min((record).confl_update_exists),
                    min((record).confl_update_deleted),
                    min((record).confl_update_missing),
                    min((record).confl_delete_origin_differs),
                    min((record).confl_delete_missing),
                    min((record).confl_multiple_unique_conflicts))::@extschema@.powa_stat_subscription_stats_history_record,
            ROW(max((record).ts),
                    max((record).apply_error_count),
                    max((record).sync_table_error_count),
                    max((record).stats_reset),
                    max((record).sync_seq_error_count),
                    max((record).confl_insert_exists),
                    max((record).confl_update_origin_differs),
                    max((record).confl_update_exists),
                    max((record).confl_update_deleted),
                    max((record).confl_update_missing),
                    max((record).confl_delete_origin_differs),
                    max((record).confl_delete_missing),
                    max((record).confl_multiple_unique_conflicts))::@extschema@.powa_stat_subscription_stats_history_record
        FROM @extschema@.powa_stat_subscription_stats_history_current
        WHERE srvid = _srvid
        GROUP BY srvid, subid;

    GET DIAGNOSTICS v_rowcount = ROW_COUNT;
    PERFORM @extschema@.powa_log(format('%s - rowcount: %s',
            v_funcname, v_rowcount));

    DELETE FROM @extschema@.powa_stat_subscription_stats_history_current WHERE srvid = _srvid;
 END;
$PROC$ LANGUAGE plpgsql; /* end of powa_stat_subscription_stats_aggregate */

CREATE OR REPLACE FUNCTION @extschema@.powa_stat_subscription_stats_history_db_div(
    a powa_stat_subscription_stats_history_db_record,
    b powa_stat_subscription_stats_history_db_record)
 RETURNS powa_stat_subscription_stats_history_rate
 IMMUTABLE STRICT
AS $PROC$
DECLARE
    res @extschema@.powa_stat_subscription_stats_history_rate;
    sec integer;
BEGIN
    res.sec = extract(EPOCH FROM (a.ts - b.ts));
    IF res.sec = 0 THEN
        sec = 1;
    ELSE
        sec = res.sec;
    END IF;
    res.apply_error_count_per_sec = (a.apply_error_count - b.apply_error_count)::double precision / sec;
    res.sync_table_error_count_per_sec = (a.sync_table_error_count - b.sync_table_error_count)::double precision / sec;
    res.sync_seq_error_count_per_sec = (a.sync_seq_error_count - b.sync_seq_error_count)::double precision / sec;
    res.confl_insert_exists_per_sec = (a.confl_insert_exists - b.confl_insert_exists)::double precision / sec;
    res.confl_update_origin_differs_per_sec = (a.confl_update_origin_differs - b.confl_update_origin_differs)::double precision / sec;
    res.confl_update_exists_per_sec = (a.confl_update_exists - b.confl_update_exists)::double precision / sec;
    res.confl_update_deleted_per_sec = (a.confl_update_deleted - b.confl_update_deleted)::double precision / sec;
    res.confl_update_missing_per_sec = (a.confl_update_missing - b.confl_update_missing)::double precision / sec;
    res.confl_delete_origin_differs_per_sec = (a.confl_delete_origin_differs - b.confl_delete_origin_differs)::double precision / sec;
    res.confl_delete_missing_per_sec = (a.confl_delete_missing - b.confl_delete_missing)::double precision / sec;
    res.confl_multiple_unique_conflicts_per_sec = (a.confl_multiple_unique_conflicts - b.confl_multiple_unique_conflicts)::double precision / sec;

    return res;
END;
$PROC$ LANGUAGE plpgsql; /* end of powa_stat_subscription_stats_history_db_div */

CREATE OR REPLACE FUNCTION @extschema@.powa_stat_subscription_stats_history_db_mi(
    a powa_stat_subscription_stats_history_db_record,
    b powa_stat_subscription_stats_history_db_record)
 RETURNS powa_stat_subscription_stats_history_diff
 IMMUTABLE STRICT
AS $PROC$
DECLARE
    res @extschema@.powa_stat_subscription_stats_history_diff;
BEGIN
    res.intvl = a.ts - b.ts;
    res.apply_error_count = a.apply_error_count - b.apply_error_count;
    res.sync_table_error_count = a.sync_table_error_count - b.sync_table_error_count;
    res.sync_seq_error_count = a.sync_seq_error_count - b.sync_seq_error_count;
    res.confl_insert_exists = a.confl_insert_exists - b.confl_insert_exists;
    res.confl_update_origin_differs = a.confl_update_origin_differs - b.confl_update_origin_differs;
    res.confl_update_exists = a.confl_update_exists - b.confl_update_exists;
    res.confl_update_deleted = a.confl_update_deleted - b.confl_update_deleted;
    res.confl_update_missing = a.confl_update_missing - b.confl_update_missing;
    res.confl_delete_origin_differs = a.confl_delete_origin_differs - b.confl_delete_origin_differs;
    res.confl_delete_missing = a.confl_delete_missing - b.confl_delete_missing;
    res.confl_multiple_unique_conflicts = a.confl_multiple_unique_conflicts - b.confl_multiple_unique_conflicts;

    return res;
END;
$PROC$ LANGUAGE plpgsql; /* end of powa_stat_subscription_stats_history_db_mi */

CREATE OR REPLACE FUNCTION @extschema@.powa_stat_subscription_stats_history_div(
    a powa_stat_subscription_stats_history_record,
    b powa_stat_subscription_stats_history_record)
  RETURNS powa_stat_subscription_stats_history_rate
  IMMUTABLE STRICT
AS $PROC$
DECLARE
    res @extschema@.powa_stat_subscription_stats_history_rate;
    sec integer;
BEGIN
    res.sec = extract(EPOCH FROM (a.ts - b.ts));
    IF res.sec = 0 THEN
        sec = 1;
    ELSE
        sec = res.sec;
    END IF;
    res.apply_error_count_per_sec = (a.apply_error_count - b.apply_error_count)::double precision / sec;
    res.sync_table_error_count_per_sec = (a.sync_table_error_count - b.sync_table_error_count)::double precision / sec;
    res.sync_seq_error_count_per_sec = (a.sync_seq_error_count - b.sync_seq_error_count)::double precision / sec;
    res.confl_insert_exists_per_sec = (a.confl_insert_exists - b.confl_insert_exists)::double precision / sec;
    res.confl_update_origin_differs_per_sec = (a.confl_update_origin_differs - b.confl_update_origin_differs)::double precision / sec;
    res.confl_update_exists_per_sec = (a.confl_update_exists - b.confl_update_exists)::double precision / sec;
    res.confl_update_deleted_per_sec = (a.confl_update_deleted - b.confl_update_deleted)::double precision / sec;
    res.confl_update_missing_per_sec = (a.confl_update_missing - b.confl_update_missing)::double precision / sec;
    res.confl_delete_origin_differs_per_sec = (a.confl_delete_origin_differs - b.confl_delete_origin_differs)::double precision / sec;
    res.confl_delete_missing_per_sec = (a.confl_delete_missing - b.confl_delete_missing)::double precision / sec;
    res.confl_multiple_unique_conflicts_per_sec = (a.confl_multiple_unique_conflicts - b.confl_multiple_unique_conflicts)::double precision / sec;

    return res;
END;
$PROC$ LANGUAGE plpgsql; /* end of powa_stat_subscription_stats_history_div */

CREATE OR REPLACE FUNCTION @extschema@.powa_stat_subscription_stats_history_mi(
    a powa_stat_subscription_stats_history_record,
    b powa_stat_subscription_stats_history_record)
 RETURNS powa_stat_subscription_stats_history_diff
 IMMUTABLE STRICT
AS $PROC$
DECLARE
    res @extschema@.powa_stat_subscription_stats_history_diff;
BEGIN
    res.intvl = a.ts - b.ts;
    res.apply_error_count = a.apply_error_count - b.apply_error_count;
    res.sync_table_error_count = a.sync_table_error_count - b.sync_table_error_count;
    res.sync_seq_error_count = a.sync_seq_error_count - b.sync_seq_error_count;
    res.confl_insert_exists = a.confl_insert_exists - b.confl_insert_exists;
    res.confl_update_origin_differs = a.confl_update_origin_differs - b.confl_update_origin_differs;
    res.confl_update_exists = a.confl_update_exists - b.confl_update_exists;
    res.confl_update_deleted = a.confl_update_deleted - b.confl_update_deleted;
    res.confl_update_missing = a.confl_update_missing - b.confl_update_missing;
    res.confl_delete_origin_differs = a.confl_delete_origin_differs - b.confl_delete_origin_differs;
    res.confl_delete_missing = a.confl_delete_missing - b.confl_delete_missing;
    res.confl_multiple_unique_conflicts = a.confl_multiple_unique_conflicts - b.confl_multiple_unique_conflicts;

    return res;
END;
$PROC$ LANGUAGE plpgsql; /* end of powa_stat_subscription_stats_history_mi */

-------------------------------
-- data sources generic support
-------------------------------
CREATE FUNCTION @extschema@.powa_generic_module_setup(_pg_module text,
                                                      _counter_cols text[],
                                                      _nullable text[] DEFAULT '{}',
                                                      _need_operators boolean default true,
                                                      _key_cols text[] DEFAULT '{}',
                                                      _key_nullable boolean DEFAULT false,
                                                      _min_version integer DEFAULT 0)
RETURNS void AS
$$
DECLARE
    i integer;
    v_nb integer;
    v_module text;
    v_sql text;
    v_colname text;
    v_coltype text;
    v_kind text;
    v_null text;
    v_has_no_minmax_col bool;
    v_suffix text;
    v_accum text;
BEGIN
    IF quote_ident(_pg_module) != _pg_module THEN
        RAISE EXCEPTION '% require quoting, which is not supported',
                         _pg_module;
    END IF;

    IF _pg_module !~ '^pg_' THEN
        RAISE EXCEPTION '% is not a postgres module', _pg_module;
    END IF;

    v_module := regexp_replace(_pg_module, '^pg', 'powa');

    -- declare the module and its configuration
    INSERT INTO @extschema@.powa_modules VALUES (_pg_module, _min_version);
    INSERT INTO @extschema@.powa_module_config VALUES (0, _pg_module);
    INSERT INTO @extschema@.powa_module_functions VALUES
        (_pg_module, 'snapshot',  v_module || '_snapshot',  v_module || '_src', false),
        (_pg_module, 'aggregate', v_module || '_aggregate', NULL,               false),
        (_pg_module, 'purge',     v_module || '_purge',     NULL,               false),
        (_pg_module, 'reset',     v_module || '_reset',     NULL,               false);

    -- create the underlying record datatype(s) and operators if needed
    EXECUTE @extschema@.powa_generic_datatype_setup(v_module, _counter_cols,
                                                    _need_operators => _need_operators);
    -- create the *_src_tmp unlogged table
    v_sql := format('CREATE UNLOGGED TABLE @extschema@.%I (
    srvid integer NOT NULL,
    ts timestamp with time zone NOT NULL',
                    v_module || '_src_tmp');

    -- iterate over the key columns first
    IF _key_nullable THEN
        v_null := '';
    ELSE
        v_null := ' NOT NULL';
    END IF;
    FOR i IN 1..coalesce(array_upper(_key_cols, 1), 0) LOOP
        v_colname := _key_cols[i][1];
        v_coltype := _key_cols[i][2];

        -- as this is the first iteration over the columns, make sure that none
        -- of them require quoting
        IF quote_ident(v_colname) != v_colname THEN
            RAISE EXCEPTION '% require quoting, which is not supported',
                            v_colname;
        END IF;

        -- key columns should only use a few of native system types
        IF v_coltype NOT IN ('boolean', 'integer', 'name', 'oid', 'text')
        THEN
            RAISE EXCEPTION 'invalid data type % for key col %.%',
                            v_coltype, v_module, v_colname;
        END IF;

        IF v_colname = ANY (_nullable) THEN
            RAISE EXCEPTION 'invalid nullable info for key col %.%',
                            v_module, v_colname;
        END IF;

        v_sql := v_sql || ',' || chr(10) || format('    %I %s%s',
                                                   v_colname, v_coltype, v_null);
    END LOOP;

    -- then iterate over the counter columns
    v_has_no_minmax_col := false;
    FOR i IN 1..array_upper(_counter_cols, 1) LOOP
        v_colname := _counter_cols[i][1];
        v_coltype := _counter_cols[i][2];

        IF v_coltype IN ('xid', 'boolean') THEN
            v_has_no_minmax_col := true;
        END IF;

        -- as this is the first iteration over the columns, make sure that none
        -- of them require quoting
        IF quote_ident(v_colname) != v_colname THEN
            RAISE EXCEPTION '% require quoting, which is not supported',
                            v_colname;
        END IF;

        -- datasources should only use a few of native system types
        IF v_coltype NOT IN ('timestamp with time zone', 'oid',  'bigint',
                             'integer', 'numeric', 'double precision',
                             'text', 'inet', 'xid', 'pg_lsn', 'interval',
                             'boolean')
        THEN
            RAISE EXCEPTION 'invalid data type % for col %.%',
                            v_coltype, v_module, v_colname;
        END IF;

        IF v_colname = ANY (_nullable) THEN
            _nullable := array_remove(_nullable, v_colname);
            v_null := '';
        ELSE
            v_null := ' NOT NULL';
        END IF;
        v_sql := v_sql || ',' || chr(10) || format('    %I %s%s',
                                                   v_colname, v_coltype,
                                                   v_null);
    END LOOP;

    IF array_upper(_nullable, 1) IS NOT NULL THEN
        RAISE EXCEPTION 'Columns % declared as nullable, but not found in the '
                        'list of columns', _nullable;
    END IF;

    v_sql := v_sql || ')';
    EXECUTE v_sql;

    -- create the *_history table and its index
    v_suffix := v_module || '_history_record';
    IF v_has_no_minmax_col THEN
        v_suffix := v_suffix || '_minmax';
    END IF;
    v_sql := format('CREATE TABLE @extschema@.%1$I (
    srvid integer NOT NULL,', v_module || '_history');

    IF _key_nullable THEN
        v_null := '';
    ELSE
        v_null := ' NOT NULL';
    END IF;
    FOR i IN 1..coalesce(array_upper(_key_cols, 1), 0) LOOP
        v_sql := v_sql || format('
    %I %s%s,', _key_cols[i][1], _key_cols[i][2], v_null);
    END LOOP;

    v_sql := v_sql || format('
    coalesce_range tstzrange NOT NULL,
    records @extschema@.%2$I[] NOT NULL,
    mins_in_range @extschema@.%3$I NOT NULL,
    maxs_in_range @extschema@.%3$I NOT NULL,
    FOREIGN KEY (srvid) REFERENCES @extschema@.powa_servers(id)
      MATCH FULL ON UPDATE CASCADE ON DELETE CASCADE
);
ALTER TABLE @extschema@.%1$I ALTER COLUMN mins_in_range SET STORAGE MAIN;
ALTER TABLE @extschema@.%1$I ALTER COLUMN maxs_in_range SET STORAGE MAIN;
CREATE INDEX %4$I ON @extschema@.%1$I USING gist(srvid, coalesce_range);',
                    v_module || '_history', v_module || '_history_record',
                    v_suffix, v_module || '_history_ts');
    EXECUTE v_sql;

    -- and the *_history_current table and index
    v_accum = 'srvid integer NOT NULL,';
    IF _key_nullable THEN
        v_null := '';
    ELSE
        v_null := ' NOT NULL';
    END IF;
    FOR i IN 1..coalesce(array_upper(_key_cols, 1), 0) LOOP
        v_colname := _key_cols[i][1];
        v_coltype := _key_cols[i][2];

        v_accum := v_accum || format('
    %I %s%s,', v_colname, v_coltype, v_null);
    END LOOP;
    v_sql := format('CREATE TABLE @extschema@.%1$I (
    %3$s
    record @extschema@.%2$I NOT NULL,
    FOREIGN KEY (srvid) REFERENCES @extschema@.powa_servers(id)
      MATCH FULL ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE INDEX ON @extschema@.%1$I (srvid);',
    v_module || '_history_current', v_module || '_history_record', v_accum);
    EXECUTE v_sql;

    -- make sure the *_history and  *_history_current tables are dumped
    PERFORM pg_catalog.pg_extension_config_dump('@extschema@.' || v_module || '_history','');
    PERFORM pg_catalog.pg_extension_config_dump('@extschema@.' || v_module || '_history_current','');

    -- create the *_snapshot function
    v_accum := '_srvid';
    FOR i IN 1..coalesce(array_upper(_key_cols, 1), 0) LOOP
        v_colname := _key_cols[i][1];

        v_accum := v_accum || format(', %I', v_colname);
    END LOOP;
    v_sql := format('CREATE FUNCTION @extschema@.%1$I (_srvid integer) RETURNS void AS $PROC$
DECLARE
    result boolean;
    v_funcname    text := format(''@extschema@.%%I(%%s)'',
                                 %1$L, _srvid);
    v_rowcount    bigint;
BEGIN
    PERFORM @extschema@.powa_log(format(''running %%s'', v_funcname));

    PERFORM @extschema@.powa_prevent_concurrent_snapshot(_srvid);

    -- Insert background writer statistics
    WITH rel AS (
        SELECT *
        FROM @extschema@.%2$I(_srvid)
    )
    INSERT INTO @extschema@.%3$I
        SELECT %4$s,
        ROW(ts',
                  v_module || '_snapshot',
                  v_module || '_src',
                  v_module || '_history_current',
                  v_accum);

    FOR i IN 1..array_upper(_counter_cols, 1) LOOP
        v_colname := _counter_cols[i][1];

        IF i > 1 AND (i - 1) % 3 = 0 THEN
            v_sql := v_sql || ',' || chr(10) || '            ';
        ELSE
            v_sql := v_sql || ', ';
        END IF;
        v_sql := v_sql || v_colname;
    END LOOP;

    v_sql := v_sql || format(')::@extschema@.%I AS record
        FROM rel;

    GET DIAGNOSTICS v_rowcount = ROW_COUNT;
    PERFORM @extschema@.powa_log(format(''%%s - rowcount: %%s'',
            v_funcname, v_rowcount));

    IF (_srvid != 0) THEN
        DELETE FROM @extschema@.%I WHERE srvid = _srvid;
    END IF;

    result := true;
END;
$PROC$ language plpgsql;',
                    v_module || '_history_record', v_module || '_src_tmp');
    EXECUTE v_sql;

    -- create the *_aggregate function
    v_accum := 'srvid';
    FOR i IN 1..coalesce(array_upper(_key_cols, 1), 0) LOOP
        v_colname := _key_cols[i][1];
        v_coltype := _key_cols[i][2];

        v_sql := v_sql || format(',
            (record).%I', v_colname);

        v_accum := v_accum || ', ' || v_colname;
    END LOOP;
    v_sql := format('CREATE FUNCTION @extschema@.%1$I(_srvid integer)
RETURNS void AS $PROC$
DECLARE
    v_funcname    text := format(''@extschema@.%%I(%%s)'',
                                 %1$L, _srvid);
    v_rowcount    bigint;
BEGIN
    PERFORM @extschema@.powa_log(format(''running %%s'', v_funcname));

    PERFORM @extschema@.powa_prevent_concurrent_snapshot(_srvid);

    -- aggregate %3$s history table
    INSERT INTO @extschema@.%2$I
        SELECT %4$s,
            tstzrange(min((record).ts), max((record).ts),''[]''),
            array_agg(record)',
                    v_module || '_aggregate', v_module || '_history',
                    v_module, v_accum);

    FOREACH v_kind IN ARRAY ARRAY['min', 'max'] LOOP
        v_sql := v_sql || format(',
            ROW(%s((record).ts)', v_kind);

        FOR i IN 1..array_upper(_counter_cols, 1) LOOP
            v_colname := _counter_cols[i][1];
            v_coltype := _counter_cols[i][2];

            IF v_coltype NOT IN ('xid', 'boolean') THEN
                v_sql := v_sql || format(',
                    %s((record).%I)', v_kind, v_colname);
            END IF;
        END LOOP;

        v_sql := v_sql || format(')::@extschema@.%I',
                                 v_suffix);
    END LOOP;

    v_sql := v_sql || format('
        FROM @extschema@.%1$I
        WHERE srvid = _srvid
        GROUP BY %2$s;

    GET DIAGNOSTICS v_rowcount = ROW_COUNT;
    PERFORM @extschema@.powa_log(format(''%%s - rowcount: %%s'',
            v_funcname, v_rowcount));

    DELETE FROM @extschema@.%1$I WHERE srvid = _srvid;
 END;
$PROC$ LANGUAGE plpgsql',
                    v_module || '_history_current', v_accum);
    EXECUTE v_sql;

    -- create the *_purge function
    v_sql := format('CREATE FUNCTION @extschema@.%1$I(_srvid integer)
RETURNS void AS $PROC$
DECLARE
    v_funcname    text := format(''@extschema@.%%I(%%s)'',
                                 %1$L, _srvid);
    v_rowcount    bigint;
    v_retention   interval;
BEGIN
    PERFORM @extschema@.powa_log(format(''running %%s'', v_funcname));

    PERFORM @extschema@.powa_prevent_concurrent_snapshot(_srvid);

    SELECT @extschema@.powa_get_server_retention(_srvid,%3$L,''module''::@extschema@.datasource_type) INTO v_retention;

    -- Delete obsolete datas. We only bother with already coalesced data
    DELETE FROM @extschema@.%2$I
    WHERE upper(coalesce_range)< (now() - v_retention)
    AND srvid = _srvid;

    GET DIAGNOSTICS v_rowcount = ROW_COUNT;
    PERFORM @extschema@.powa_log(format(''%%s - rowcount: %%s'',
            v_funcname, v_rowcount));
END;
$PROC$ LANGUAGE plpgsql
        ',
                    v_module || '_purge', v_module || '_history', _pg_module);
    EXECUTE v_sql;

    -- create the *_reset function
    v_sql := format('CREATE FUNCTION @extschema@.%1$I(_srvid integer)
RETURNS boolean AS $function$
BEGIN
    PERFORM @extschema@.powa_log(''Resetting %2$I('' || _srvid || '')'');
    DELETE FROM @extschema@.%2$I WHERE srvid = _srvid;

    PERFORM @extschema@.powa_log(''Resetting %3$I('' || _srvid || '')'');
    DELETE FROM @extschema@.%3$I WHERE srvid = _srvid;

    PERFORM @extschema@.powa_log(''Resetting %4$I('' || _srvid || '')'');
    DELETE FROM @extschema@.%4$I WHERE srvid = _srvid;

    RETURN true;
END;
$function$ LANGUAGE plpgsql',
                    v_module || '_reset', v_module || '_history',
                    v_module || '_history_current', v_module || '_src_tmp');
    EXECUTE v_sql;
END;
$$ LANGUAGE plpgsql
SET search_path = pg_catalog; /* end of powa_generic_module_setup */


CREATE FUNCTION @extschema@.powa_generic_datatype_setup(
    _datasource text,
    _cols text[],
    _extra jsonb DEFAULT '{}',
    _need_operators boolean default true
)
RETURNS void AS
$$
DECLARE
    i integer;
    v_prefix text;
    v_sql text;
    v_record_name text;
    v_colname text;
    v_coltype text;
    v_extra text;
    v_kind text;
    c_no_agg text[];
    v_has_no_agg_col bool;
    c_no_minmax text[];
    v_has_no_minmax_col bool;
BEGIN
    IF quote_ident(_datasource) != _datasource THEN
        RAISE EXCEPTION '% require quoting, which is not supported',
                        _datasource;
    END IF;

    -- we don't put any fields for any of those datatypes in the *_history_db
    -- records, as aggregating them per-database or computing a rate wouldn't
    -- make sense
    c_no_agg := ARRAY['timestamp with time zone', 'timestamptz'];
    -- Similarly, don't put any field with datatypes that don't support min/max
    -- in the mins_in_range / maxs_in_range records
    c_no_minmax := ARRAY['xid', 'boolean'];

    -- we loop over the whole process in case we find some columns with
    -- some specific datatypes.  For those we need to create a *_history_db
    -- version datatype without such columns, as we can't do a per-db
    -- aggregation of such data.
    -- Similarly, we need to create a specific datatype for the min/max
    -- aggregated records for datatypes that don't support min/max.
    v_has_no_agg_col := false;
    v_has_no_minmax_col := false;
    FOREACH v_prefix IN ARRAY ARRAY['_history', '_history_db'] LOOP
        -- we only need _db version of the infrastructure if we skipped some
        -- columns
        EXIT WHEN v_prefix = '_history_db' AND NOT v_has_no_agg_col;

        v_record_name := _datasource || v_prefix || '_record';

        -- first, create a main record type
        -- as this is the first iteration over the columns, make sure that none
        -- of them require quoting
        v_sql := format('CREATE TYPE @extschema@.%I AS (
ts timestamp with time zone',
                        v_record_name);
        FOR i IN 1..array_upper(_cols, 1) LOOP
            v_colname := _cols[i][1];
            v_coltype := _cols[i][2];

            -- the *_history_db type version is the same as the *_history
            -- without the fields of datatype we don't aggregate
            CONTINUE WHEN v_prefix = '_history_db'
                     AND v_coltype = ANY (c_no_agg);

            IF quote_ident(v_colname) != v_colname THEN
                RAISE EXCEPTION '% require quoting, which is not supported',
                                v_colname;
            END IF;

            -- datasources should only use a few of native system types
            IF v_coltype NOT IN ('timestamp with time zone', 'oid',  'bigint',
                                 'integer', 'numeric', 'double precision',
                                 'text', 'inet', 'xid', 'pg_lsn', 'interval',
                                 'boolean')
            THEN
                RAISE EXCEPTION 'invalid data type % for col %.%',
                                v_coltype, _datasource, v_colname;
            END IF;

            IF v_coltype = ANY (c_no_minmax) THEN
                v_has_no_minmax_col := true;
            END IF;

            v_sql := v_sql || ',' || chr(10) || v_colname || ' ' || v_coltype;
        END LOOP;
        v_sql := v_sql || ')';
        EXECUTE v_sql;

        -- Create a specific min/max record if needed
        IF v_prefix = '_history' AND v_has_no_minmax_col THEN
            v_sql := format('CREATE TYPE @extschema@.%I AS (
ts timestamp with time zone',
                            v_record_name || '_minmax');
            FOR i IN 1..array_upper(_cols, 1) LOOP
                v_colname := _cols[i][1];
                v_coltype := _cols[i][2];

                CONTINUE WHEN v_coltype = ANY (c_no_minmax);

                v_sql := v_sql || ',' || chr(10) || v_colname || ' ' || v_coltype;
            END LOOP;
            v_sql := v_sql || ')';
            EXECUTE v_sql;
        END IF;

        CONTINUE WHEN NOT _need_operators;

        -- add a *history_rate and a *history_diff type, and remember if we saw
        -- (and skipped) any timestamptz column
        FOREACH v_kind IN ARRAY ARRAY['diff', 'rate'] LOOP
            CONTINUE WHEN v_prefix = '_history_db';

            IF v_kind = 'diff' THEN
                v_extra := 'intvl interval';
            ELSE
                v_extra := 'sec integer';
            END IF;
            v_sql := format('CREATE TYPE @extschema@.%I AS ('
                            || chr(10)
                            || '%s',
                            _datasource || v_prefix || '_' || v_kind, v_extra);
            FOR i IN 1..array_upper(_cols, 1) LOOP
                v_colname := _cols[i][1];
                v_coltype := _cols[i][2];

                IF v_coltype = ANY (c_no_agg)
                THEN
                    v_has_no_agg_col = true;
                    CONTINUE;
                END IF;

                v_colname = coalesce(_extra->v_kind->'colname'->>v_colname,
                                     v_colname);

                IF v_kind = 'rate' THEN
                    v_colname := v_colname ||
                        coalesce(_extra->v_kind->'suffix'->>v_colname,
                                 '_per_sec');
                    IF v_coltype != 'numeric' THEN
                        v_coltype := 'double precision';
                    END IF;
                END IF;

                v_sql := v_sql || ', ' || chr(10)
                    || quote_ident(v_colname) || ' ' || v_coltype;
            END LOOP;
            v_sql := v_sql || ')';
            EXECUTE v_sql;
        END LOOP;

        -- add a *_mi() function
        v_sql := format('CREATE FUNCTION @extschema@.%1$I(
a @extschema@.%2$I,
b @extschema@.%2$I)
RETURNS @extschema@.%3$I AS
$_$
DECLARE
    res @extschema@.%3$I;
BEGIN
    res.intvl = a.ts - b.ts;',
                        _datasource || v_prefix || '_mi',
                        v_record_name,
                        -- the datatype is the same for the _db version
                        _datasource || '_history_diff');
        FOR i in 1..array_upper(_cols, 1) LOOP
            CONTINUE WHEN _cols[i][2] = ANY (c_no_agg);
            v_colname := _cols[i][1];
            v_colname = coalesce(_extra->'mi'->'colname'->>v_colname,
                                 v_colname);

            v_sql := v_sql || chr(10)
                || format('    res.%1$I = a.%1$I - b.%1$I;', v_colname);
        END LOOP;
        v_sql := v_sql || chr(10) || chr(10) || '    return res;'
            || chr(10) || 'END;' || chr(10) || '$_$'
            || chr(10) || 'LANGUAGE plpgsql IMMUTABLE STRICT';
        EXECUTE v_sql;

        -- add a "-" operator
        v_sql := format('CREATE OPERATOR @extschema@.- ('
                        'PROCEDURE = @extschema@.%1$I,'
                        'LEFTARG = @extschema@.%2$I,'
                        'RIGHTARG = @extschema@.%2$I)',
                        _datasource || v_prefix || '_mi',
                        v_record_name);
        EXECUTE v_sql;

        -- add a *_div() function
        v_sql := format('CREATE FUNCTION @extschema@.%1$I(
a @extschema@.%2$I,
b @extschema@.%2$I)
RETURNS @extschema@.%3$I AS
$_$
DECLARE
    res @extschema@.%3$I;
    sec integer;
BEGIN
    res.sec = extract(EPOCH FROM (a.ts - b.ts));
    IF res.sec = 0 THEN
        sec = 1;
    ELSE
        sec = res.sec;
    END IF;',
                        _datasource || v_prefix || '_div',
                        v_record_name,
                        -- the datatype is the same for the _db version
                        _datasource || '_history_rate');
        FOR i in 1..array_upper(_cols, 1) LOOP
            CONTINUE WHEN _cols[i][2] = ANY (c_no_agg);

            v_colname := _cols[i][1];
            v_extra := coalesce(_extra->'rate'->'colname'->>v_colname,
                                v_colname);
            v_extra := v_extra ||
                coalesce(_extra->'rate'->'suffix'->>v_colname, '_per_sec');

            v_sql := v_sql || chr(10)
                || format('    res.%1$I = (a.%2$I - b.%2$I)::double precision / sec;',
                          v_extra, v_colname);
    END LOOP;
    v_sql := v_sql || '

    return res;
END;
$_$
LANGUAGE plpgsql IMMUTABLE STRICT';
    EXECUTE v_sql;

    -- add a "/" operator
    v_sql := format('CREATE OPERATOR @extschema@./ ('
                    'PROCEDURE = @extschema@.%1$I,'
                    'LEFTARG = @extschema@.%2$I,'
                    'RIGHTARG = @extschema@.%2$I)',
                    _datasource || v_prefix || '_div',
                    v_record_name);
    EXECUTE v_sql;
    END LOOP;
END;
$$ LANGUAGE plpgsql
SET search_path = pg_catalog; /* end of powa_generic_datatype_setup */

-------------------
-- pg_stat_recovery
-------------------
SELECT @extschema@.powa_generic_module_setup('pg_stat_recovery',
$${
{last_replayed_read_lsn, pg_lsn}, {last_replayed_end_lsn, pg_lsn},
{last_replayed_tli, integer},
{replay_end_lsn, pg_lsn}, {replay_end_tli, integer},
{recovery_last_xact_time, timestamp with time zone},
{current_chunk_start_time, timestamp with time zone},
{pause_state, text}
}$$,
$${
last_replayed_read_lsn, last_replayed_end_lsn, last_replayed_tli,
replay_end_lsn, replay_end_tli,
recovery_last_xact_time, current_chunk_start_time, pause_state
}$$,
-- pg_stat_recovery only exists on pg19+
_min_version => 190000
);

CREATE OR REPLACE FUNCTION @extschema@.powa_stat_recovery_src(IN _srvid integer,
    OUT ts timestamp with time zone,
    OUT last_replayed_read_lsn pg_lsn,
    OUT last_replayed_end_lsn pg_lsn,
    OUT last_replayed_tli integer,
    OUT replay_end_lsn pg_lsn,
    OUT replay_end_tli integer,
    OUT recovery_last_xact_time timestamp with time zone,
    OUT current_chunk_start_time timestamp with time zone,
    OUT pause_state text
) RETURNS SETOF record STABLE AS $PROC$
DECLARE
    v_current_lsn pg_lsn;
    v_pg_version_num int;
BEGIN
    IF (_srvid = 0) THEN
        v_pg_version_num := current_setting('server_version_num')::int;

        -- pg19+, view is added
        IF v_pg_version_num >= 190000 THEN
            RETURN QUERY SELECT now,
                s.last_replayed_read_lsn,
                s.last_replayed_end_lsn,
                s.last_replayed_tli,
                s.replay_end_lsn,
                s.replay_end_tli,
                s.recovery_last_xact_time,
                s.current_chunk_start_time,
                s.pause_state
            FROM (SELECT now() AS now) n
            LEFT JOIN pg_catalog.pg_stat_recovery AS s ON true;
        ELSE
            RETURN QUERY SELECT now(),
                '0/0'::pg_lsn AS last_replayed_read_lsn,
                '0/0'::pg_lsn AS last_replayed_end_lsn,
                0::integer AS last_replayed_tli,
                '0/0'::pg_lsn AS replay_end_lsn,
                0::integer AS replay_end_tli,
                NULL::timestamp with time zone AS recovery_last_xact_time,
                NULL::timestamp with time zone AS current_chunk_start_time,
                NULL::text AS pause_state
            WHERE false;
        END IF;
    ELSE
        RETURN QUERY SELECT s.ts,
                s.last_replayed_read_lsn,
                s.last_replayed_end_lsn,
                s.last_replayed_tli,
                s.replay_end_lsn,
                s.replay_end_tli,
                s.recovery_last_xact_time,
                s.current_chunk_start_time,
                s.pause_state
        FROM @extschema@.powa_stat_recovery_src_tmp AS s
        WHERE s.srvid = _srvid;
    END IF;
END;
$PROC$ LANGUAGE plpgsql; /* end of powa_stat_recovery_src */

-------------------
-- pg_stat_lock
-------------------
SELECT @extschema@.powa_generic_module_setup('pg_stat_lock',
$${
{waits, bigint}, {wait_time, bigint},
{fastpath_exceeded, bigint},
{stats_reset, timestamp with time zone}
}$$,
$${
stats_reset
}$$,
_key_cols => $${
{locktype, text}
}$$,
-- pg_stat_lock only exists on pg19+
_min_version => 190000
);

CREATE OR REPLACE FUNCTION @extschema@.powa_stat_lock_src(IN _srvid integer,
    OUT ts timestamp with time zone,
    OUT locktype text,
    OUT waits bigint,
    OUT wait_time bigint,
    OUT fastpath_exceeded bigint,
    OUT stats_reset timestamp with time zone
) RETURNS SETOF record STABLE AS $PROC$
DECLARE
    v_current_lsn pg_lsn;
    v_pg_version_num int;
BEGIN
    IF (_srvid = 0) THEN
        v_pg_version_num := current_setting('server_version_num')::int;

        -- pg19+, view is added
        IF v_pg_version_num >= 190000 THEN
            RETURN QUERY SELECT now,
                s.locktype,
                s.waits, s.wait_time,
                s.fastpath_exceeded,
                s.stats_reset
            FROM (SELECT now() AS now) n
            LEFT JOIN pg_catalog.pg_stat_lock AS s ON true;
        ELSE
            RETURN QUERY SELECT now(),
                NULL::text AS locktype,
                0::bigint AS waits,
                0::bigint AS wait_time,
                0::bigint AS fastpath_exceeded,
                NULL::timestamp with time zone AS stats_reset
            WHERE false;
        END IF;
    ELSE
        RETURN QUERY SELECT s.ts,
                s.locktype,
                s.waits,
                s.wait_time,
                s.fastpath_exceeded,
                s.stats_reset
        FROM @extschema@.powa_stat_lock_src_tmp AS s
        WHERE s.srvid = _srvid;
    END IF;
END;
$PROC$ LANGUAGE plpgsql; /* end of powa_stat_lock_src */


---------------------------------------
-- cleanup data sources generic support
---------------------------------------
DROP FUNCTION @extschema@.powa_generic_datatype_setup(text, text[], jsonb, boolean);
DROP FUNCTION @extschema@.powa_generic_module_setup(text, text[], text[], boolean, text[], boolean, integer);

-- Fix the toast tuple targets
SELECT @extschema@.powa_fix_toast_tuple_target();
