/*
 * Author: DBRE - Joe Smith
 * Date: Sep 2022
 * Purpose: sets up tables, functions and extensions for index maintenance when using uuids.
*/

-- required to schedule job
create extension pg_cron;

-- required to calculate bloat
create extension pgstattuple;

-- table defines indexes to maintain, target bloat percentage which triggers maintenance, and an acceptable window to
-- do maintenance if the bloat is met during hours we dont want to run the maintenance
CREATE TABLE index_maintenance_configuration (
    id int primary key,
    index_name varchar(512),
    maintenance_threshold_percent float,
    man_window_start timestamp,
    man_window_end timestamp
);

-- table tracks number of times maintenance is done.
-- when maintenance was done
CREATE TABLE index_maintenance (
    index_name varchar(512),
    index_iteration bigint,
    maintenance_done timestamp,
    scheduled boolean
    PRIMARY KEY(index_name, index_iteration, scheduled)
);

-- no maintenance if you don't have a config
ALTER TABLE index_maintenance
    ADD FOREIGN KEY (index_name)
    REFERENCES index_maintenance_configuration(index_name);

-- returns bloat as float for an index.
create or replace function return_idx_bloat (idx varchar(512))
    language plpgsql
    as
    $$
    RETURNS float AS $$
    declare
        current_bloat float;

    begin
        SELECT 100-(pgstatindex(idx)).avg_leaf_density into current_bloat;
        return current_bloat;
    end;
$$;

-- calls the re-index function, concurrently.
-- raises a notice in logs which can be parsed from cloud watch
-- guarantees date is between dates, casts then to timestamps.
create or replace function call_reindex(ind varchar(512), man_window_start timestamp,
    man_window_end timestamp)
    language plpgsql
    as
    $$
    RETURNS boolean AS $$
    declare
          today timestamp;
          new_index bigint;
    begin
        today = now();

        if not man_window_start then;
            -- see call_reindex function
            RAISE NOTICE 'call_reindex(%, no_man_window_specified)', idx;
            REINDEX CONCURRENTLY idx;
            return true;
        end if;

        -- if the time of the run is within the right window, we re-index.
        -- we return the same iteration to close out that record.
        if today::timestamp between man_window_start::timestamp and man_window_end::timestamp;
            RAISE NOTICE 'call_reindex(%, %)', idx, today;
            REINDEX CONCURRENTLY idx;
            return true;
        else;
            return false;
        end if;
    end;
$$;


create or replace function do_idx_maintenance(idx varchar(512))
    language plpgsql
    as
    $$
    RETURNS RECORD AS $$
    declare
        current_iteration bigint;
        new_iteration bigint;
        is_scheduled boolean;
        man_window_start timestamp;
        man_window_end timestamp;
        maintenance_threshold_percent float;
        current_bloat float;
        maintenance_ran boolean;

    begin

        -- look for configuration for current ind
        maintenance_threshold_percent, man_window_start, man_window_end = select maintenance_threshold_percent,
            man_window_start. man_window_end
            from index_maintenance_configuration
            where index_name = idx;

        -- check for a current iteration to increment, if none, then we start one at 0.
        SELECT max(index_iteration), scheduled INTO current_iteration, is_scheduled FROM index_maintenance WHERE index_name = idx;

        -- set iteration of current record if one does not exist.
        -- other wise, we use the current_iteration from the table.
        if not current_iteration then;
            current_iteration = 0;
        end if;

        -- no maintenance window is allowed and will reindex each time.
        if not man_window_start then;
            -- see call_reindex function
            maintenance_ran = call_reindex(idx, man_window_start, man_window_end);

        end if;

        -- if the row we get back is scheduled, we try to run it.
        if is_scheduled;

            maintenance_ran = call_reindex(idx, man_window_start, man_window_end);


        else;
            -- here we have no row that is scheduled.
            -- we have a maintenance window to check.
            -- we check the current bloat, and try to re-index.
            current_bloat = return_idx_bloat(idx);

            if current_bloat > maintenance_threshold_percent then;
                maintenance_ran = call_reindex(idx, man_window_start, man_window_end);
            end if;

        end if;

        -- using simple logic, we will either increment the maintenance row, or create a new one.
        return maintenance_ran, current_iteration;

    end;
$$;

create or replace function uuid_idx_maintenance()
    language plpgsql
    as
    $$
    declare
       idx_array varchar[];

    begin
       -- return all indexes we are to maintain, if none, we do nothing.
       select array_agg( distinct idx_name )
            into idx_array
            from index_maintenance_configuration;

       -- if there are no indexes, its a no op.
       if  array_length(idx_array) > 0 then;

            -- iterate over each name in array
            foreach idx in array idx_array loop

                -- define variable for current iteration
                maintenance_done boolean;
                current_iteration bigint;
                
                rec record;

                -- call function to check idx, return record with multiple results.
                -- parse out information into variables.

                rec = do_idx_maintenance(idx);
                maintenance_done = rec[0]
                current_iteration = rec[1]

                -- maintenance_done coming back true means the maintenance was done.
                -- maintenance_done coming back false means that there was no maintenance done.
                if maintenance_done then;
                    UPDATE set maintenance_done = now(), scheduled = false
                        WHERE index_name = idx (idx, current_iteration, now())
                        AND index_iteration = current_iteration;
                else;
                -- this record will violate a primary key if a row already exists.
                -- otherwise, it schedules a row for the next run to review.
                    INSERT into
                        index_maintenance(index_name, index_iteration, maintenance_done, scheduled )
                        VALUES (idx, current_iteration,null, true) on conflict do nothing;
                end if;

                continue;

                end loop;
       else;
            return
       end if;
    end;
$$;

