/**
 * PostgreSQL pgsql_tweaks extension
 * Licence:    PostgreSQL Licence, see https://raw.githubusercontent.com/sjstoelting/pgsql-tweaks/master/LICENSE.md
 * Author:     Stefanie Janine Stölting <mail@stefanie-stoelting.de>
 * Repository: http://github.com/sjstoelting/pgsql_tweaks/
 * Version:    1.0.3
 */

/*** Initial statements ***/
SET client_min_messages TO warning;
SET log_min_messages    TO warning;

/*** Create a schema for the extension ***/
CREATE SCHEMA IF NOT EXISTS pgsql_tweaks;

/*** Grant rights to role public for all objects ***/
GRANT USAGE ON SCHEMA pgsql_tweaks TO public;

ALTER DEFAULT PRIVILEGES IN SCHEMA pgsql_tweaks GRANT SELECT ON TABLES TO public;

ALTER DEFAULT PRIVILEGES IN SCHEMA pgsql_tweaks GRANT EXECUTE ON FUNCTIONS TO public;

/*** Set search path to have all objects in the schema pgsql_tweaks ***/
SET search_path TO pgsql_tweaks, public;


/*** files with creation statements ***/

/**
 * Creates two functions to check strings for being a date.
 * The first function checks it with the default format, the second with the
 * format given as parameter.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_date(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::date;
	RETURN TRUE;
EXCEPTION WHEN OTHERS THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_date(s text) IS 'Takes a text and checks if it is a date, uses standard date format YYYY-MM-DD';


CREATE OR REPLACE FUNCTION is_date(s text, f text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM to_date(s, f);
	RETURN TRUE;
EXCEPTION WHEN OTHERS THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_date(s text, f text) IS 'Takes a text and checks if it is a date by taking the second text as date format';

/**
 * Creates two functions to check strings for being a time.
 * The first function checks it with the default format, the second with the
 * format given as parameter.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_time(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::TIME;
	RETURN TRUE;
EXCEPTION WHEN OTHERS THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_time(s text) IS 'Takes a text and checks if it is a time, uses standard date format HH24:MI:SS.US';


CREATE OR REPLACE FUNCTION is_time(s text, f text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM to_timestamp(s, f)::TIME;
	RETURN TRUE;
EXCEPTION WHEN OTHERS THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_time(s text, f text) IS 'Takes a text and checks if it is a time by taking the second text as time format';

/**
 * Creates two functions to check strings for being timestamps.
 * The first function checks it with the default format, the second with the
 * format given as parameter.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_timestamp(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::TIMESTAMP;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE;
COMMENT ON FUNCTION is_timestamp(s text) IS 'Takes a text and checks if it is a timestamp, uses standard timestamp format YYYY-MM-DD HH24:MI:SS';


CREATE OR REPLACE FUNCTION is_timestamp(s text, f text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM to_timestamp(s, f)::TIMESTAMP;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE;
COMMENT ON FUNCTION is_timestamp(s text) IS 'Takes a text and checks if it is a timestamp by taking the second text as date format';

/**
 * Creates a function to check strings for being nunbers.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */
CREATE OR REPLACE FUNCTION is_numeric(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::NUMERIC;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_numeric(s text) IS 'Checks, whether the given parameter is a number';

/**
 * Creates a function to check strings for being BIGINT.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_bigint(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::BIGINT;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_bigint(s text) IS 'Checks, whether the given parameter is a BIGINT';

/**
 * Creates a function to check strings for being INTEGER.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_integer(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::INTEGER;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_integer(s text) IS 'Checks, whether the given parameter is an INTEGER';

/**
 * Creates a function to check strings for being SMALLINT.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_smallint(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::SMALLINT;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_smallint(s text) IS 'Checks, whether the given parameter is a smallint';

/**
 * Creates a function to check strings for being INTEGER.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_real(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::REAL;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_real(s text) IS 'Checks, whether the given parameter is a REAL';

/**
 * Creates a function to check strings for being INTEGER.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_double_precision(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::DOUBLE PRECISION;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_double_precision(s text) IS 'Checks, whether the given parameter is a DOUBLE PRECISION';

/**
 * Creates a function to check strings for being BOOLEAN.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_boolean(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::BOOLEAN;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_boolean(s text) IS 'Checks, whether the given parameter is a BOOLEAN';

/**
 * Creates a function to check strings for being JSON.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_json(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::JSON;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_json(s text) IS 'Checks, whether the given text is a JSON';

/**
 * Creates a function to check strings for being JSONB.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_jsonb(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::JSONB;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_jsonb(s text) IS 'Checks, whether the given text is a JSONB';

/**
 * Creates a function to check strings for being UUID.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_uuid(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::UUID;
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_uuid(s text) IS 'Checks, whether the given parameter is a uuid';

/**
 * Creates a function which returns the size of a schema.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION pg_schema_size(text) RETURNS BIGINT AS $$
	SELECT COALESCE(SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename)))::BIGINT, 0) AS schema_size
	FROM pg_tables
	WHERE schemaname = $1
$$ LANGUAGE SQL STRICT IMMUTABLE;
COMMENT ON FUNCTION pg_schema_size(text) IS 'Returns the size for given schema name';

/**
 * Creates two functions to check strings about encodings.
 * The first function checks if an UTF-8 string does only contain characters
 * in the given second parameter.
 * The second parameter takes as third parameter the encoding in which the
 * string is and checks if the string does only contain characters as given in
 * the second parameter.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_encoding(s text, enc text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM convert(s::bytea, 'UTF8', enc);
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_encoding(s text, enc text) IS 'Checks, whether the given UTF8 sting contains only encodings in the given encoding characters';


CREATE OR REPLACE FUNCTION is_encoding(s text, enc text, enc_from text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM convert(s::bytea, enc_from, enc);
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_encoding(s text, enc text, enc_from text) IS 'Checks, whether the given encoding sting contains only encodings in the given encoding characters';

/**
 * Creates a function to check UTF-8 strings for containing only Latin1
 * characters.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_latin1(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM convert(s::bytea, 'UTF8', 'LATIN1');
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_latin1(s text) IS 'Checks, whether the given parameter contains only latin1 characters';

/**
 * Creates a function which returns a distinct array with all non latin1
 * characters . Depends on function is_latin1 which is part of this repository.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION return_not_part_of_latin1(s text) RETURNS text[] AS $$
DECLARE
	i INTEGER := 0;
	res text[];
	current_s text := NULL::text[];
BEGIN

	LOOP
		EXIT WHEN i > length(s);
		i := i + 1;
		current_s := substring(s FROM i FOR 1);
		IF (NOT is_latin1(current_s)) THEN
			SELECT array_append(res, current_s) INTO res;
		END IF;
	END LOOP;

	WITH t1 AS
		(
			SELECT unnest(res) AS c1
		)
	, t2 AS
		(
			SELECT DISTINCT c1
			FROM t1
		)
	, t3 AS
		(
			SELECT array_agg(c1) AS res_array
			FROM t2
		)
	SELECT res_array
	FROM t3
	INTO res;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION return_not_part_of_latin1(s text) IS 'Creates a function which returns a distinct array with all non latin1 characters';

/**
 * Creates three function to replace characters, that are not part of the given
 * encoding.
 * The function does depend on the function is_encoding which is part of this
 * repository.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION replace_encoding(s text, e text) RETURNS text AS $$
DECLARE
	i INTEGER := 0;
	res text;
BEGIN
	res := s;

	LOOP
		EXIT WHEN i > length(res);
		i := i + 1;
		IF (NOT is_encoding(substring(res FROM i FOR 1 ), e)) THEN
			res := OVERLAY(res PLACING '' FROM i FOR 1);
		END IF;
	END LOOP;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION replace_encoding(s text, e text) IS 'Replaces all characters, which are not part of the given encoding, with spaces and returns the result only with characters which are part of the given encoding';


CREATE OR REPLACE FUNCTION replace_encoding(s text, e text, replacement text) RETURNS text AS $$
DECLARE
	i INTEGER := 0;
	res text;
BEGIN
	res := s;

	LOOP
		EXIT WHEN i > length(res);
		i := i + 1;
		IF (NOT is_encoding(substring(res FROM i FOR 1 ), e)) THEN
			res := OVERLAY(res PLACING replacement FROM i FOR 1);
		END IF;
	END LOOP;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION replace_encoding(s text, e text, replacement text) IS 'Replaces all characters, which are not part of the given encoding, with the given replacement in the third parameter and returns the result only with characters which are part of the given encoding';


CREATE OR REPLACE FUNCTION replace_encoding(s text, s_search text[], s_replace text[]) RETURNS text AS $$
DECLARE
	i INTEGER := 0;
	res text;
	length_equal BOOLEAN;
	a_count INTEGER;
BEGIN

	SELECT array_length(s_search, 1) = array_length(s_replace, 1) INTO length_equal;

	IF NOT length_equal THEN
		RAISE 'Search and replacement arrays do not have the same count of entries' USING ERRCODE = '22000';
	END IF;

	SELECT array_length(s_search, 1) INTO a_count;
	res := s;

	LOOP
		EXIT WHEN i >= a_count;
		i := i + 1;

		res := REPLACE(res, s_search[i], s_replace[i]);

	END LOOP;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION replace_encoding(s text, s_search text[], s_replace text[]) IS 'Replaces charactes given in s_search with characters given in s_replace at the same array position';

/**
 * Creates two function to replace characters, that are not part of latin1.
 * The function does depend on the function is_latin1 which is part of this
 * repository.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION replace_latin1(s text) RETURNS text AS $$
DECLARE
	i INTEGER := 0;
	res text;
BEGIN
	res := s;

	LOOP
		EXIT WHEN i > length(res);
		i := i + 1;
		IF (NOT is_latin1(substring(res FROM i FOR 1 ))) THEN
			res := OVERLAY(res PLACING '' FROM i FOR 1);
		END IF;
	END LOOP;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION replace_latin1(s text) IS 'Replaces all not latin1 characters with spaces and returns the result with only containing latin1 characters';


CREATE OR REPLACE FUNCTION replace_latin1(s text, replacement text) RETURNS text AS $$
DECLARE
	i INTEGER := 0;
	res text;
BEGIN
	res := s;

	LOOP
		EXIT WHEN i > length(res);
		i := i + 1;
		IF (NOT is_latin1(substring(res FROM i FOR 1 ))) THEN
			res := OVERLAY(res PLACING replacement FROM i FOR 1);
		END IF;
	END LOOP;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION replace_latin1(s text, replacement text) IS 'Replaces all not latin1 characters with the given replacement in the second parameter and returns the result with only containing latin1 characters';


CREATE OR REPLACE FUNCTION replace_latin1(s text, s_search text[], s_replace text[]) RETURNS text AS $$
DECLARE
	res text;
BEGIN

	SELECT replace_encoding(s, s_search, s_replace) INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION replace_latin1(s text, s_search text[], s_replace text[]) IS 'Replaces charactes given in s_search with characters given in s_replace at the same array position. The function is an alias for replace_encoding.';

/**
 * Creates a function which returns a distinct array with all non latin1 characters .
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION return_not_part_of_encoding(s text, e text) RETURNS text[] AS $$
DECLARE
	i INTEGER := 0;
	res text[];
	current_s text := NULL::text[];
BEGIN

	LOOP
		EXIT WHEN i > length(s);
		i := i + 1;
		current_s := substring(s FROM i FOR 1);
		IF (NOT is_encoding(current_s, e)) THEN
			SELECT array_append(res, current_s) INTO res;
		END IF;
	END LOOP;

	WITH t1 AS
		(
			SELECT unnest(res) AS c1
		)
	, t2 AS
		(
			SELECT DISTINCT c1
			FROM t1
		)
	, t3 AS
		(
			SELECT array_agg(c1) AS res_array
			FROM t2
		)
	SELECT res_array
	FROM t3
	INTO res;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION return_not_part_of_encoding(s text, e text) IS 'Creates a function which returns a distinct array with all characters which are not part of the encoding give in e';

/**
 * Create a window function to calculate values for gaps.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION gap_fill_internal(s anyelement, v anyelement)
RETURNS anyelement AS
$$
BEGIN
	RETURN COALESCE(v, s);
END;
$$ LANGUAGE PLPGSQL IMMUTABLE;
COMMENT ON FUNCTION gap_fill_internal(s anyelement, v anyelement) IS 'The function is used to fill gaps in window functions';


-- The Window function needs an aggregate
DROP AGGREGATE IF EXISTS gap_fill(anyelement);
CREATE AGGREGATE gap_fill(anyelement) (
	SFUNC=gap_fill_internal,
	STYPE = anyelement
)
;
COMMENT ON AGGREGATE gap_fill(anyelement) IS 'Implements the aggregate function to fill gaps using the function GapFillInternal';

/**
 * Creates a function which returns the given date in German format.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

 CREATE OR REPLACE FUNCTION date_de(d DATE) RETURNS text AS $$
 BEGIN
	RETURN to_char(d, 'DD.MM.YYYY');
 END;
 $$
 STRICT
 LANGUAGE plpgsql IMMUTABLE;
 COMMENT ON FUNCTION date_de(d DATE) IS 'Creates a function which returns the given date in German format';

/**
 * Creates a function which returns the given timestamp in German format.
 * The second parameter indicates, if the result is with or without time zone,
 * default is with thime zone
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

 CREATE OR REPLACE FUNCTION datetime_de(t TIMESTAMP WITH TIME ZONE, with_tz BOOLEAN DEFAULT TRUE) RETURNS text AS $$
 BEGIN
 	IF with_tz THEN
 		RETURN to_char(t, 'DD.MM.YYYY HH24:MI:SS TZ');
 	ELSE
 		RETURN to_char(t, 'DD.MM.YYYY HH24:MI:SS');
 	END IF;
 END;
 $$
 STRICT
 LANGUAGE plpgsql IMMUTABLE;
 COMMENT ON FUNCTION datetime_de(t TIMESTAMP WITH TIME ZONE, with_tz BOOLEAN) IS 'Creates a function which returns the given timestamp in German format';

/**
 * Creates two functions which returns unix timestamp for the a given timestamp
 * or a given timestamp with time zone.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION to_unix_timestamp(ts timestamp) RETURNS bigint AS $$
	SELECT EXTRACT (EPOCH FROM ts)::bigint;
$$ LANGUAGE SQL STRICT IMMUTABLE
;
COMMENT ON FUNCTION to_unix_timestamp(ts timestamp) IS 'Returns an unix timestamp for the given timestamp';

CREATE OR REPLACE FUNCTION to_unix_timestamp(ts timestamp with time zone) RETURNS bigint AS $$
	SELECT EXTRACT (EPOCH FROM ts)::bigint;
$$ LANGUAGE SQL STRICT IMMUTABLE
;
COMMENT ON FUNCTION to_unix_timestamp(ts timestamp with time zone) IS 'Returns an unix timestamp for the given timestamp with time zone';

/**
 * Creates a function to checks a string variable for being either, NULL or ''.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_empty(s text) RETURNS BOOLEAN AS $f1$
BEGIN
        RETURN COALESCE(s, '') = '';
END;
$f1$
LANGUAGE plpgsql IMMUTABLE
;

COMMENT ON FUNCTION is_empty(s text) IS 'Checks, whether the given parameter is NULL or ''''';

/**
 * Returns the maximum value of an array.
 * Implementation for BIGINT, INTEGER, SMALLINT, TEXT
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

-- BIGINT implementation
CREATE OR REPLACE FUNCTION array_max(a BIGINT[]) RETURNS BIGINT AS $$
DECLARE
	res BIGINT;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
	SELECT max(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_max(a BIGINT[]) IS 'Returns the maximum value of a BIGINT array';

-- INTEGER implementation
CREATE OR REPLACE FUNCTION array_max(a INTEGER[]) RETURNS BIGINT AS $$
DECLARE
	res INTEGER;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT max(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_max(a INTEGER[]) IS 'Returns the maximum value of an INTEGER array';

-- SMALLINT implementation
CREATE OR REPLACE FUNCTION array_max(a SMALLINT[]) RETURNS BIGINT AS $$
DECLARE
	res SMALLINT;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT max(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_max(a SMALLINT[]) IS 'Returns the maximum value of a SMALLINT array';

-- TEXT implementation
CREATE OR REPLACE FUNCTION array_max(a TEXT[]) RETURNS TEXT AS $$
DECLARE
	res TEXT;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT max(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_max(a TEXT[]) IS 'Returns the maximum value of a TEXT array';

-- REAL implementation
CREATE OR REPLACE FUNCTION array_max(a REAL[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT max(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_max(a REAL[]) IS 'Returns the maximum value of a REAL array';

-- DOUBLE PRECISION implementation
CREATE OR REPLACE FUNCTION array_max(a DOUBLE PRECISION[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT max(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_max(a DOUBLE PRECISION[]) IS 'Returns the maximum value of a DOUBLE PRECISION array';

-- NUMERIC implementation
CREATE OR REPLACE FUNCTION array_max(a NUMERIC[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT max(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_max(a NUMERIC[]) IS 'Returns the maximum value of a NUMERIC array';

/**
 * Returns the minumum value of an array.
 * Implementation for BIGINT, INTEGER, SMALLINT, TEXT
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

-- BIGINT implementation
CREATE OR REPLACE FUNCTION array_min(a BIGINT[]) RETURNS BIGINT AS $$
DECLARE
	res BIGINT;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
	SELECT min(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_min(a BIGINT[]) IS 'Returns the minumum value of a BIGINT array';

-- INTEGER implementation
CREATE OR REPLACE FUNCTION array_min(a INTEGER[]) RETURNS INTEGER AS $$
DECLARE
	res INTEGER;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT min(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_min(a INTEGER[]) IS 'Returns the minumum value of an INTEGER array';

-- SMALLINT implementation
CREATE OR REPLACE FUNCTION array_min(a SMALLINT[]) RETURNS SMALLINT AS $$
DECLARE
	res SMALLINT;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT min(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_min(a SMALLINT[]) IS 'Returns the minumum value of a SMALLINT array';

-- TEXT implementation
CREATE OR REPLACE FUNCTION array_min(a TEXT[]) RETURNS TEXT AS $$
DECLARE
	res TEXT;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT min(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_min(a TEXT[]) IS 'Returns the minumum value of a TEXT array';

-- REAL implementation
CREATE OR REPLACE FUNCTION array_min(a REAL[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT min(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_min(a REAL[]) IS 'Returns the minumum value of a REAL array';

-- DOUBLE PRECISION implementation
CREATE OR REPLACE FUNCTION array_min(a DOUBLE PRECISION[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT min(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_min(a DOUBLE PRECISION[]) IS 'Returns the minumum value of a DOUBLE PRECISION array';

-- NUMERIC implementation
CREATE OR REPLACE FUNCTION array_min(a NUMERIC[]) RETURNS NUMERIC AS $$
DECLARE
	res REAL;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT min(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_min(a NUMERIC[]) IS 'Returns the minumum value of a NUMERIC array';

/**
 * Returns the average value of an array.
 * Implementation for BIGINT, INTEGER, SMALLINT
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

-- BIGINT implementation
CREATE OR REPLACE FUNCTION array_avg(a BIGINT[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
	SELECT avg(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_avg(a BIGINT[]) IS 'Returns the average value of a BIGINT array';

-- INTEGER implementation
CREATE OR REPLACE FUNCTION array_avg(a INTEGER[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT avg(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_avg(a INTEGER[]) IS 'Returns the average value of an INTEGER array';

-- SMALLINT implementation
CREATE OR REPLACE FUNCTION array_avg(a SMALLINT[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT avg(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_avg(a SMALLINT[]) IS 'Returns the average value of a SMALLINT array';

--REAL implementation
CREATE OR REPLACE FUNCTION array_avg(a REAL[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT avg(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_avg(a REAL[]) IS 'Returns the average value of a REAL array';

-- DOUBLE PRECISION implementation
CREATE OR REPLACE FUNCTION array_avg(a DOUBLE PRECISION[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT avg(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_avg(a DOUBLE PRECISION[]) IS 'Returns the average value of a DOUBLE PRECISION array';

-- NUMERIC implementation
CREATE OR REPLACE FUNCTION array_avg(a NUMERIC[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT avg(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_avg(a NUMERIC[]) IS 'Returns the average value of a NUMERIC array';

/**
 * Returns the sum of values of an array.
 * Implementation for BIGINT, INTEGER, SMALLINT
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

-- BIGINT implementation
CREATE OR REPLACE FUNCTION array_sum(a BIGINT[]) RETURNS BIGINT AS $$
DECLARE
	res BIGINT;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
	SELECT sum(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_sum(a BIGINT[]) IS 'Returns the sum of values of a BIGINT array';

-- INTEGER implementation
CREATE OR REPLACE FUNCTION array_sum(a INTEGER[]) RETURNS BIGINT AS $$
DECLARE
	res BIGINT;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT sum(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_sum(a INTEGER[]) IS 'Returns the sum of values of an INTEGER array';

-- SMALLINT implementation
CREATE OR REPLACE FUNCTION array_sum(a SMALLINT[]) RETURNS BIGINT AS $$
DECLARE
	res BIGINT;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT sum(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_sum(a SMALLINT[]) IS 'Returns the sum of values of a SMALLINT array';

-- REAL implementation
CREATE OR REPLACE FUNCTION array_sum(a REAL[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT sum(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_sum(a REAL[]) IS 'Returns the sum of values of a REAL array';

-- DOUBLE PRECISION implementation
CREATE OR REPLACE FUNCTION array_sum(a DOUBLE PRECISION[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT sum(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_sum(a DOUBLE PRECISION[]) IS 'Returns the sum of values of a DOUBLE PRECISION array';

-- NUMERIC implementation
CREATE OR REPLACE FUNCTION array_sum(a NUMERIC[]) RETURNS NUMERIC AS $$
DECLARE
	res NUMERIC;
BEGIN

	WITH unnested AS
 		(
			SELECT UNNEST(a) AS vals
		)
		SELECT sum(vals) FROM unnested AS x INTO res;

	RETURN res;

END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_sum(a NUMERIC[]) IS 'Returns the sum of values of a NUMERIC array';

/**
 * Removes empty strings and null entries from a given array. In addition the
 * function can remove duplicate entries. The function supports strings,
 * numbers, dates, and timestamps with or without time zone
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION array_trim(a text[], rd BOOLEAN DEFAULT FALSE) RETURNS text[] AS $$
DECLARE
	res text[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE vals != ''
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE vals != ''
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a text[], rd BOOLEAN) IS 'Removes empty entries from a text array, can remove duplicates, too';

CREATE OR REPLACE FUNCTION array_trim(a SMALLINT[], rd BOOLEAN DEFAULT FALSE) RETURNS SMALLINT[] AS $$
DECLARE
	res SMALLINT[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a SMALLINT[], rd BOOLEAN) IS 'Removes empty entries from a SMALLINT array, can remove duplicates, too';

CREATE OR REPLACE FUNCTION array_trim(a INTEGER[], rd BOOLEAN DEFAULT FALSE) RETURNS INTEGER[] AS $$
DECLARE
	res INTEGER[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a INTEGER[], rd BOOLEAN) IS 'Removes empty entries from a INTEGER array, can remove duplicates, too';

CREATE OR REPLACE FUNCTION array_trim(a BIGINT[], rd BOOLEAN DEFAULT FALSE) RETURNS BIGINT[] AS $$
DECLARE
	res BIGINT[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a BIGINT[], rd BOOLEAN) IS 'Removes empty entries from a BIGINT array, can remove duplicates, too';

CREATE OR REPLACE FUNCTION array_trim(a NUMERIC[], rd BOOLEAN DEFAULT FALSE) RETURNS NUMERIC[] AS $$
DECLARE
	res NUMERIC[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a NUMERIC[], rd BOOLEAN) IS 'Removes empty entries from a NUMERIC array, can remove duplicates, too';

CREATE OR REPLACE FUNCTION array_trim(a REAL[], rd BOOLEAN DEFAULT FALSE) RETURNS REAL[] AS $$
DECLARE
	res REAL[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a REAL[], rd BOOLEAN) IS 'Removes empty entries from a REAL array, can remove duplicates, too';

CREATE OR REPLACE FUNCTION array_trim(a DOUBLE PRECISION[], rd BOOLEAN DEFAULT FALSE) RETURNS DOUBLE PRECISION[] AS $$
DECLARE
	res DOUBLE PRECISION[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a DOUBLE PRECISION[], rd BOOLEAN) IS 'Removes empty entries from a DOUBLE PRECISION array, can remove duplicates, too';

CREATE OR REPLACE FUNCTION array_trim(a DATE[], rd BOOLEAN DEFAULT FALSE) RETURNS DATE[] AS $$
DECLARE
	res DATE[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a DATE[], rd BOOLEAN) IS 'Removes empty entries from a DATE array, can remove duplicates, too';

CREATE OR REPLACE FUNCTION array_trim(a TIMESTAMP[], rd BOOLEAN DEFAULT FALSE) RETURNS TIMESTAMP[] AS $$
DECLARE
	res TIMESTAMP[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a TIMESTAMP[], rd BOOLEAN) IS 'Removes empty entries from a TIMESTAMP array, can remove duplicates, too';

CREATE OR REPLACE FUNCTION array_trim(a TIMESTAMP WITH TIME ZONE[], rd BOOLEAN DEFAULT FALSE) RETURNS TIMESTAMP WITH TIME ZONE[] AS $$
DECLARE
	res TIMESTAMP WITH TIME ZONE[];
BEGIN

	IF rd THEN
		WITH t1 AS
			(
				SELECT DISTINCT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	ELSE
		WITH t1 AS
			(
				SELECT unnest(a) AS vals
			)
		SELECT array_agg(vals)
		FROM t1
		WHERE NOT vals IS NULL
		INTO res;
	END IF;

	RETURN res;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION array_trim(a TIMESTAMP WITH TIME ZONE[], rd BOOLEAN) IS 'Removes empty entries from a TIMESTAMP WITH TIME ZONE array, can remove duplicates, too';

/**
 * Creates a function to convert a hexadicimal number given as string into a BIGINT.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

 CREATE OR REPLACE FUNCTION hex2bigint(s TEXT) RETURNS BIGINT AS $$
 	WITH RECURSIVE hex_sep AS
 		(
 			SELECT
 			  	cast (cast (cast ('x0' || substring (s, length (s), 1) AS BIT(8)) AS INT) * POWER(16, 0) AS BIGINT ) int_res
 				, substring (s, 1 , length (s) - 1) AS rest
 				, length (s) - 1 AS len
 				, 1 AS row_num
 			UNION ALL
 			SELECT
 				cast (cast (cast ('x0' || substring (rest, length (rest), 1) AS BIT(8)) AS INT)  * POWER(16, row_num) AS BIGINT) int_res
 				, substring (rest, 1 , length (rest) - 1) AS rest
 				, length (rest) - 1 AS len
 				, row_num + 1 AS row_num
 			FROM hex_sep
 			WHERE len > 0
 		)
 	SELECT cast (sum(int_res)AS BIGINT)
 	FROM hex_sep
 	;
 $$ LANGUAGE sql IMMUTABLE STRICT
 ;
COMMENT ON FUNCTION hex2bigint(s TEXT) IS 'Converts a hexadicimal number given as string into a BIGINT';

/**
 * Creates a function to check hexadeciaml numbers passed as strings for being hexadeciaml fitting into a 0BIGINT.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_hex(s TEXT) RETURNS BOOLEAN AS $$
BEGIN
   PERFORM hex2bigint (s);
   RETURN TRUE;
EXCEPTION WHEN others THEN
   RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_hex(s TEXT) IS 'Checks, whether the given parameter is a hexadeciaml number fitting into a BIGINT';

/**
 * Creates a function to check strings for being a BIGINT array.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_bigint_array(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::BIGINT[];
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_bigint_array(s text) IS 'Checks, whether the given parameter is a BIGINT array';

/**
 * Creates a function to check strings for being an INTEGER array.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_integer_array(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::INTEGER[];
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_integer_array(s text) IS 'Checks, whether the given parameter is an INTEGER array';

/**
 * Creates a function to check strings for being an SMALLINT array.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_smallint_array(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::SMALLINT[];
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_smallint_array(s text) IS 'Checks, whether the given parameter is a SMALLINT array';

/**
 * Creates a function to check strings for being an SMALLINT array.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION is_text_array(s text) RETURNS BOOLEAN AS $$
BEGIN
	PERFORM s::TEXT[];
	RETURN TRUE;
EXCEPTION WHEN others THEN
	RETURN FALSE;
END;
$$
STRICT
LANGUAGE plpgsql IMMUTABLE
;
COMMENT ON FUNCTION is_text_array(s text) IS 'Checks, whether the given parameter is a TEXT array';

/**
 * Function to generate a documentation from database comments as Markdown for
 * a given Schema.
 * The result is a text in Markdown format.
 *
 * Currently the following objects are supported:
 * 	- Database
 * 	- Schema
 * 	- Tables with columns
 * 	- Views with columns
 * 	- Materialized Views with columns
 * 	- Foreign Tables with columns
 * 	- Functions
 * 	- Procedures
 *
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION get_markdown_doku_by_schema(in_schema_name TEXT, time_zone TEXT DEFAULT 'Europe/Berlin')
RETURNS TEXT
LANGUAGE PLPGSQL
AS
$$
DECLARE
	res TEXT DEFAULT '';
	current_table_name TEXT DEFAULT '';
	current_table_type TEXT DEFAULT '';
	current_program_name TEXT DEFAULT '';
	current_mv_name TEXT DEFAULT '';
	table_header TEXT;
	time_zone_exists BOOLEAN;
	curs_tables CURSOR FOR
		WITH indexes AS
			(
				SELECT schemaname
					, tablename
					, array_agg(indexname) AS indexes
				FROM pg_indexes
				GROUP BY schemaname
					, tablename
			)
		SELECT t.table_type
			, c.table_catalog AS database_name
			, c.table_schema AS schema_name
			, c.table_name
			, c.column_name
			, COALESCE (array_to_string (i.indexes, ';'), '') AS indexes
			, COALESCE (c.column_default, '') AS column_default
			, c.is_nullable
			, c.data_type
			, COALESCE (c.character_maximum_length::TEXT, '') AS character_maximum_length
			, COALESCE (de.description, '') AS database_description
			, COALESCE (obj_description(('"' || c.table_schema || '"')::regnamespace), '') AS schema_description
			, COALESCE (OBJ_DESCRIPTION(('"' || c.table_schema || '"."' || c.table_name || '"')::regclass), '') AS table_description
			, COALESCE (COL_DESCRIPTION(('"' || c.table_schema || '"."' || c.table_name || '"')::regclass, c.ordinal_position), '') AS column_description
		FROM information_schema.columns AS c
		INNER JOIN information_schema."tables" AS t
			USING (table_catalog, table_schema, table_name)
		INNER JOIN pg_catalog.pg_database AS d
			ON c.table_catalog = d.datname
		LEFT OUTER JOIN pg_catalog.pg_shdescription AS de
			ON d."oid" = de.objoid		LEFT OUTER JOIN indexes AS i
			ON c.table_schema = i.schemaname
			AND c.table_name = i.tablename
		WHERE c.table_schema = in_schema_name
		ORDER BY t.table_type
			, c.table_name
			, c.ordinal_position
		;
	table_record RECORD;
	curs_programs CURSOR FOR
		SELECT (current_database())::information_schema.sql_identifier AS database_name
			, pg_namespace.nspname AS schema_name
			, pg_proc.proname AS program_name
			, COALESCE (pg_catalog.pg_get_function_arguments(pg_proc.oid), '') AS parameters
			, COALESCE (pg_catalog.pg_get_function_result(pg_proc.oid), '') AS returning_data_type
			, CASE
					WHEN pg_proc.prokind = 'a' THEN
						'AGGREGATE'
					WHEN pg_proc.prokind = 'w' THEN
						'WINDOW'
					WHEN pg_proc.prokind = 'f' THEN
						'FUNCTION'
					WHEN pg_proc.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN
						'TRIGGER'
					ELSE
						'UNKNOWN'
				END AS program_type
			, pg_language.lanname AS program_language
			, COALESCE (pg_description.description, '') AS program_description
		FROM pg_catalog.pg_proc
		INNER JOIN pg_catalog.pg_language
			ON pg_proc.prolang = pg_language.oid
		LEFT OUTER JOIN pg_catalog.pg_namespace
			ON pg_proc.pronamespace = pg_namespace.oid
		LEFT OUTER JOIN pg_catalog.pg_description
			ON pg_proc.oid = pg_description.objoid
		WHERE pg_catalog.pg_function_is_visible(pg_proc.oid)
			AND pg_namespace.nspname = in_schema_name
		ORDER BY schema_name
			, program_name
			, parameters
		;
	program_record RECORD;
	curs_mv CURSOR FOR
		WITH indexes AS
			(
				SELECT schemaname
					, tablename
					, array_agg(indexname) AS indexes
				FROM pg_indexes
				GROUP BY schemaname
					, tablename
			)
		SELECT (current_database())::information_schema.sql_identifier AS database_name
			  , n.nspname AS schema_name
		    , c.relname AS table_name
		    , col.attname AS column_name
			, COALESCE (array_to_string (i.indexes, ';'), '') AS indexes
		    , format_type (col.atttypid, col.atttypmod) AS data_type
			, COALESCE (obj_description (c."oid"), '') AS materialized_view_description
			, COALESCE (COL_DESCRIPTION(('"' || n.nspname || '"."' || c.relname || '"')::regclass, col.attnum), '') AS column_description
		FROM pg_catalog.pg_class AS c
		INNER JOIN pg_catalog.pg_namespace AS n
			ON c.relnamespace = n.oid
		INNER JOIN pg_catalog.pg_attribute AS col
			ON c.oid = col.attrelid
		LEFT OUTER JOIN indexes AS i
			ON n.nspname = i.schemaname
			AND c.relname = i.tablename
		WHERE c.relkind = 'm'
		    AND col.attnum >= 1
		    AND n.nspname = in_schema_name
		ORDER BY schema_name
		    , table_name
		    , column_name
		;
	mv_record RECORD;
BEGIN
	-- Check given time zone for existence
	EXECUTE 'SELECT count (*) = 1 AS exist
		FROM pg_catalog.pg_timezone_names
		WHERE name::TEXT = $1'
		USING time_zone
		INTO time_zone_exists
	;

	IF NOT time_zone_exists THEN
		RAISE EXCEPTION 'Time zone % does not exist!', time_zone;
	END IF;

	table_header = '| Column | Default | Is Nullable | Data Type | Maximum Length | Description |' || CHR(13) || '|--|--|--|--|--|--|'  || CHR(13);

	-- TABLES and VIEWS
	OPEN curs_tables;

	FETCH NEXT FROM curs_tables INTO table_record;
	WHILE FOUND LOOP
		IF current_table_name = '' THEN
			res = '# DATABASE ' ||
				table_record.database_name || ' SCHEMA ' || table_record.schema_name ||
				CHR(13) || CHR(13) ||
				'Generated: ' || TO_CHAR (now() AT time ZONE time_zone, 'YYYY-MM-DD HH24:MI') ||
				CHR(13) || CHR(13) ||
				'## Database Description' ||
				CHR(13) || CHR(13) ||
				table_record.database_description ||
				CHR(13) || CHR(13) ||
				'## Schema Description' ||
				CHR(13) || CHR(13) ||
				table_record.schema_description ||
				CHR(13) || CHR(13)
			;

		END IF;

		IF current_table_type <> table_record.table_type THEN
			current_table_type = table_record.table_type;
			CASE table_record.table_type
				WHEN 'BASE TABLE' THEN
					res = res ||
						'## Tables in Schema ' || table_record.schema_name ||
						CHR(13) || CHR(13)
					;

				WHEN 'VIEW' THEN
					res = res ||
						'## Views in Schema ' || table_record.schema_name ||
						CHR(13) || CHR(13)
					;

				WHEN 'FOREIGN' THEN
					res = res ||
						'## Foreign Tables in Schema ' || table_record.schema_name ||
						CHR(13) || CHR(13)
					;
				ELSE
					res = res;
			END CASE;
		END IF;

		IF current_table_name <> table_record.table_name THEN
			current_table_name = table_record.table_name;
			res = res || '### ' || table_record.table_name
				|| CHR(13) || CHR(13)
			;

			res = res || table_record.table_description ||
				CHR(13) || CHR(13)
			;

			IF table_record.indexes <> '' THEN
				res = res || 'Indexes: ' || table_record.indexes ||
					CHR(13) || CHR(13)
				;
			END IF;

			res = res || table_header;

		END IF;

		res = res || '| ' || table_record.column_name || ' | ' ||
			table_record.column_default || ' | ' ||
			table_record.is_nullable || ' | ' ||
			table_record.data_type || ' | ' ||
			table_record.character_maximum_length || ' | ' ||
			table_record.column_description || ' |' ||
			CHR(13)
		;

		FETCH NEXT FROM curs_tables INTO table_record;
	END LOOP;

	CLOSE curs_tables;

	-- MATERIALIZED VIEWS
	OPEN curs_mv;

	FETCH NEXT FROM curs_mv INTO mv_record;
	WHILE FOUND LOOP

			IF current_mv_name = '' THEN
			res = res || CHR(13) ||
				'## MATERIALIZED VIEWS in DATABASE ' ||
				mv_record.database_name ||
				' SCHEMA ' ||
				mv_record.schema_name ||
				CHR(13) || CHR(13)
			;
		END IF;

		IF current_mv_name <> mv_record.table_name THEN
			current_mv_name = mv_record.table_name;
			res = res || '### ' || mv_record.table_name ||
				CHR(13) || CHR(13) ||
				mv_record.materialized_view_description ||
				CHR(13) || CHR(13)
			;

			IF mv_record.indexes <> '' THEN
				res = res || 'Indexes: ' || mv_record.indexes ||
					CHR(13) || CHR(13)
				;
			END IF;

			res = res || '| Column | Data Type |  | Description |' || CHR(13) ||
				'|--|--|--|'  || CHR(13)
			;
		END IF;


		res = res || '| ' || mv_record.column_name || ' | ' ||
			mv_record.data_type || ' | ' ||
			mv_record.column_description || ' |' ||
			CHR(13)
		;

		FETCH NEXT FROM curs_mv INTO mv_record;
	END LOOP;

	CLOSE curs_mv;

	-- PROGRAMS AND FUNCTIONS
	OPEN curs_programs;
	FETCH NEXT FROM curs_programs INTO program_record;
	WHILE FOUND LOOP


		IF current_program_name = '' THEN
			current_program_name = program_record.program_name;
			res = res || CHR(13) || '## FUNCTIONS and PROCEDURES in DATABASE ' || program_record.database_name || ' SCHEMA ' || program_record.schema_name ||
				CHR(13) || CHR(13) ||
				'| Program Name | Type | Language | Parameters | Returning | Description |' || CHR(13) ||
				'|--|--|--|--|--|--|' || CHR(13)
			;
		END IF;
		res = res || '| ' || program_record.program_name || ' | ' ||
			program_record.program_type || ' | ' ||
			program_record.program_language || ' | ' ||
			program_record.parameters || ' | ' ||
			program_record.returning_data_type || ' |' ||
			program_record.program_description || ' | ' ||
			CHR(13)
		;

		FETCH NEXT FROM curs_programs INTO program_record;
	END LOOP;

	CLOSE curs_programs;

	RETURN res;
END;
$$
;

COMMENT ON FUNCTION get_markdown_doku_by_schema(TEXT, TEXT) IS 'Function to generate a documentation from database comments as Markdown. The result is a text in Markdown format.';

/**
 * Returns a table contianing the inheritance path of a given rolename.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

 DO $$
 DECLARE
 	version_greater_15 BOOLEAN;
	function_source TEXT;
 BEGIN
 	SELECT to_number((string_to_array(version(), ' '))[2], '999.99') >= 16
	INTO version_greater_15;

 	IF version_greater_15 THEN
 		-- Create the function for PostgreSQL 16 or newer
		function_source :=
$string$
CREATE OR REPLACE FUNCTION role_inheritance (role_name text)
	RETURNS table("oid" oid, rolename text, steps integer, role_path text, inherit_option boolean, set_option boolean)
AS $function$
BEGIN
	RETURN QUERY
	WITH RECURSIVE rcte AS
		(
			SELECT pr.oid AS "oid"
				, 0 AS steps
				, pr.oid::regrole::text AS path
				, pr.rolinherit AS inherit_option
				, NULL::boolean AS set_option
			FROM   pg_roles AS pr
			WHERE  rolname = role_name
			UNION ALL
			SELECT m.roleid
				, rcte.steps + 1
				, rcte.path || '->' || m.roleid::regrole::text
				, rcte.inherit_option AND m.inherit_option
				, m.set_option
			FROM rcte
			INNER JOIN pg_auth_members AS m
				ON rcte."oid" = m.member
	   )
	SELECT rcte."oid"
		, rcte."oid"::regrole::text AS rolename
		, rcte.steps
		, rcte.path AS role_path
		, rcte.inherit_option
		, rcte.set_option
	FROM rcte
	;
END;
$function$
LANGUAGE plpgsql
;
$string$
;
	ELSE
		-- Create the function for PostgreSQL older than 16
		function_source :=
$string$
CREATE OR REPLACE FUNCTION role_inheritance (role_name text)
	RETURNS table("oid" oid, rolename text, steps integer, role_path text)
AS $function$
BEGIN
	RETURN QUERY
	WITH RECURSIVE rcte AS
		(
			SELECT pr.oid AS "oid"
				, 0 AS steps
				, pr.oid::regrole::text AS path
			FROM   pg_roles AS pr
			WHERE  rolname = role_name
			UNION ALL
			SELECT m.roleid
				, rcte.steps + 1
				, rcte.path || '->' || m.roleid::regrole::text
			FROM rcte
			INNER JOIN pg_auth_members AS m
				ON rcte."oid" = m.member
	   )
	SELECT rcte."oid"
		, rcte."oid"::regrole::text AS rolename
		, rcte.steps
		, rcte.path AS role_path
	FROM rcte
	;
END;
$function$
LANGUAGE plpgsql
;
$string$
;
	END IF;

	EXECUTE function_source;

	COMMENT ON FUNCTION role_inheritance(s text) IS 'Returns a table contianing the inheritance path of a given rolename.';
ENd
$$;

/**
 * Creates a view to get all views of the current database but excluding system views and all views which do start with 'pg' or '_pg'.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW pg_db_views AS
SELECT table_catalog AS view_catalog
	, table_schema AS view_schema
	, table_name AS view_name
	, view_definition
FROM INFORMATION_SCHEMA.views
WHERE NOT table_name LIKE 'pg%'
	AND NOT table_name LIKE '\\_pg%'
	AND table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY table_catalog
	, table_schema
	, table_name
;

COMMENT ON VIEW pg_db_views IS 'Creates a view to get all views of the current database but excluding system views and all views which do start with ''pg'' or ''_pg''';
COMMENT ON COLUMN pg_db_views.view_catalog IS 'The database, where the view is created in.';
COMMENT ON COLUMN pg_db_views.view_schema IS 'The schema, where the view is created in.';
COMMENT ON COLUMN pg_db_views.view_name IS 'The name of the view.';
COMMENT ON COLUMN pg_db_views.view_definition IS 'The source code of the view.';

/**
 * Creates a view to get all foreign keys of the current database.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW pg_foreign_keys AS
SELECT ccu.constraint_name
	, tc.is_deferrable
	, tc.initially_deferred
	, tc."enforced"
	, tc.table_schema
	, tc.table_name
	, kcu.column_name
	, ccu.table_schema AS foreign_table_schema
	, ccu.TABLE_NAME AS foreign_table_name
	, ccu.COLUMN_NAME AS foreign_column_name
	, EXISTS
		(
			SELECT 1
			FROM pg_catalog.pg_index AS i
			WHERE i.indrelid = cs.conrelid
				AND i.indpred IS NULL
				AND (i.indkey::smallint[])[0:cardinality(cs.conkey)-1] OPERATOR(pg_catalog.@>) cs.conkey
		) AS is_indexed
	FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
	INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu
		ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
	INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu
		ON ccu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
	INNER JOIN PG_CATALOG.PG_NAMESPACE AS n
		ON tc.table_schema = n.nspname
	INNER JOIN PG_CATALOG.PG_CONSTRAINT AS cs
		ON n."oid" = cs.connamespace
		AND tc.constraint_name = cs.conname
WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY'
;

COMMENT ON VIEW pg_foreign_keys IS 'The view returns all foreign keys of the current database';
COMMENT ON COLUMN pg_foreign_keys.constraint_name IS 'The name of the constraint.';
COMMENT ON COLUMN pg_foreign_keys.is_deferrable IS 'Whether the setting can be changed within a transaction.';
COMMENT ON COLUMN pg_foreign_keys.initially_deferred IS 'Whether each row is checked on insert or at the end of the transaction.';
COMMENT ON COLUMN pg_foreign_keys.enforced IS 'For PostgreSQL versions <18 it is alway TRUE as the feature is not availabe, starting with PostgreSQL 18 it is TRUE, when the constraint is enforced.';
COMMENT ON COLUMN pg_foreign_keys.table_schema IS 'The name of the schema';
COMMENT ON COLUMN pg_foreign_keys.table_name IS 'The name of the table.';
COMMENT ON COLUMN pg_foreign_keys.column_name IS 'Name of the column used in the foreign key constraint.';
COMMENT ON COLUMN pg_foreign_keys.foreign_table_schema IS 'The name of the foreign schema.';
COMMENT ON COLUMN pg_foreign_keys.foreign_table_name IS 'The name of the foreign table.';
COMMENT ON COLUMN pg_foreign_keys.foreign_column_name IS 'Name of the column used in the foreign key constraint of a foreign table.';
COMMENT ON COLUMN pg_foreign_keys.is_indexed IS 'Whether an index exists for the foreign key constraint, or not.';

/**
 * Creates a view to get all functions of the current database.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW pg_functions AS
SELECT pg_namespace.nspname AS schema_name
	, pg_proc.proname AS function_name
	, pg_catalog.pg_get_function_result(pg_proc.oid) AS returning_data_type
	, pg_catalog.pg_get_function_arguments(pg_proc.oid) AS parameters
	, CASE
			WHEN pg_proc.prokind = 'a' THEN
				'aggregate'
			WHEN pg_proc.prokind = 'w' THEN
				'window'
			WHEN pg_proc.prokind = 'f' THEN
				'function'
			WHEN pg_proc.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN
				'trigger'
			ELSE
				'unknown'
		END AS function_type
	, pg_description.description AS function_comment
FROM pg_catalog.pg_proc
	LEFT OUTER JOIN pg_catalog.pg_namespace
		ON pg_proc.pronamespace = pg_namespace.oid
	LEFT OUTER JOIN pg_catalog.pg_description
		ON pg_proc.oid = pg_description.objoid
WHERE pg_catalog.pg_function_is_visible(pg_proc.oid)
	AND pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY schema_name
	, function_name
	, parameters
;

-- Add a comment
COMMENT ON VIEW pg_functions IS 'The view returns all functions of the current database, excluding those in the schema pg_catalog and information_schema';
COMMENT ON COLUMN pg_functions.schema_name IS 'The name of the schema where the function is stored in.';
COMMENT ON COLUMN pg_functions.function_name IS 'The name of the function or procedure.';
COMMENT ON COLUMN pg_functions.returning_data_type IS 'The data type that the function is returning.';
COMMENT ON COLUMN pg_functions.parameters IS 'Parameters used to call the function.';
COMMENT ON COLUMN pg_functions.function_type IS 'The type of function, can be aggregate, window, function, trigger, or unknown.';
COMMENT ON COLUMN pg_functions.function_comment IS 'The comment, that is added to descrbe the function.';

/**
 * Creates a view to get information about table and materialized views in the
 * current database. It includes their sizes and indexes.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW pg_table_matview_infos AS
WITH indexes AS
	(
		SELECT schemaname
			, tablename
			, array_agg(indexname) AS indexes
		FROM pg_indexes
		GROUP BY schemaname
			, tablename
	)
SELECT 'table' AS type
	, n.nspname AS schema_name
	, c.relname AS object_name
	, pg_get_userbyid (c.relowner) AS object_owner
	, t.spcname AS TABLESPACE
	, i.indexes
	, pg_table_size (c.oid) AS object_size
	, pg_indexes_size(c.oid) AS indexes_size
	, pg_total_relation_size(c.oid) AS total_object_size
	, pg_size_pretty(pg_table_size(c.oid)) AS total_object_size_pretty
	, pg_size_pretty(pg_indexes_size(c.oid)) AS indexes_size_pretty
	, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_relation_size_pretty
FROM pg_class AS c
	LEFT OUTER JOIN pg_namespace AS n
		ON n.oid = c.relnamespace
	LEFT OUTER JOIN pg_tablespace AS t
		ON t.oid = c.reltablespace
	LEFT OUTER JOIN indexes AS i
		ON n.nspname = i.schemaname
		AND c.relname = i.tablename
WHERE c.relkind = ANY (ARRAY['r'::"char", 'p'::"char"])
	AND n.nspname NOT IN ('pg_catalog', 'information_schema')
UNION ALL
SELECT 'matview' AS type
	, n.nspname AS schema_name
	, c.relname AS object_name
	, pg_get_userbyid(c.relowner) AS object_owner
	, t.spcname AS tablespace
	, i.indexes
	, pg_table_size(c.oid) AS object_size
	, pg_indexes_size(c.oid) AS indexes_size
	, pg_total_relation_size(c.oid) AS total_object_size
	, pg_size_pretty(pg_table_size(c.oid)) AS object_size_pretty
	, pg_size_pretty(pg_indexes_size(c.oid)) AS indexes_size_pretty
	, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_relation_size_pretty
FROM pg_class AS c
	LEFT OUTER JOIN pg_namespace AS n
		ON n.oid = c.relnamespace
	LEFT OUTER JOIN pg_tablespace t
		ON t.oid = c.reltablespace
	LEFT OUTER JOIN indexes AS i
		ON n.nspname = i.schemaname
		AND c.relname = i.tablename
WHERE c.relkind = 'm'::"char"
;

COMMENT ON VIEW pg_table_matview_infos IS 'The view shows detailed information about sizes and indexes of tables and materialized views';
COMMENT ON COLUMN pg_table_matview_infos.type IS 'The type of the result row, can be TABLE or MATERIALIZED VIEW.';
COMMENT ON COLUMN pg_table_matview_infos.schema_name IS 'The name of the schema where the object is stored.';
COMMENT ON COLUMN pg_table_matview_infos.object_name IS 'The name of the table or materialized view.';
COMMENT ON COLUMN pg_table_matview_infos.object_owner IS 'The role which is owning the obejct.';
COMMENT ON COLUMN pg_table_matview_infos.tablespace IS 'The table space where the object is stored.';
COMMENT ON COLUMN pg_table_matview_infos.indexes IS 'All indexes that have been created on this object.';
COMMENT ON COLUMN pg_table_matview_infos.object_size IS 'The size of the data of this object.';
COMMENT ON COLUMN pg_table_matview_infos.indexes_size IS 'The size of the indexes of this object.';
COMMENT ON COLUMN pg_table_matview_infos.total_object_size IS 'The overall size of this object.';
COMMENT ON COLUMN pg_table_matview_infos.total_object_size_pretty IS 'The size of the data of this object with unit.';
COMMENT ON COLUMN pg_table_matview_infos.indexes_size_pretty IS 'The size of the indexes of this object with unit.';
COMMENT ON COLUMN pg_table_matview_infos.total_relation_size_pretty IS 'The overall size of this object with the unit.';

/**
 * Creates a view to get information about partitioned tables.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	version_greater_10 BOOLEAN;
BEGIN
	SELECT to_number((string_to_array(version(), ' '))[2], '999.99') >= 10 INTO version_greater_10;

	IF version_greater_10 THEN
		-- Create the view pg_functions for PostgreSQL 10 or newer
		CREATE OR REPLACE VIEW pg_partitioned_tables_infos AS
		SELECT cl.oid AS parent_relid
			, n.nspname AS parent_schemaname
			, cl.relname AS parent_tablename
			, r.rolname AS parent_owner
			, CASE pt.partstrat
				WHEN 'l' THEN
					'LIST'
				WHEN 'r' THEN
					'RANGE'
				WHEN 'h' THEN
					'HASH'
			END AS partition_strategy
			, count (cl2.oid) OVER (PARTITION BY cl.oid) AS count_of_partitions
			, COALESCE (sum (pg_relation_size (cl2.oid)) OVER (PARTITION BY cl.oid), 0) AS overall_size
			, cl2.oid AS child_relid
			, n2.nspname AS child_schemaname
			, cl2.relname AS child_tablename
			, r2.rolname AS child_owner
			, pg_relation_size (cl2.oid) AS child_size
		FROM pg_catalog.pg_class AS cl
		INNER JOIN pg_catalog.pg_partitioned_table AS pt
			ON cl.oid = pt.partrelid
		INNER JOIN pg_catalog.pg_namespace AS n
			ON cl.relnamespace = n.oid
		INNER JOIN pg_catalog.pg_roles AS r
			 ON cl.relowner = r.oid
		LEFT OUTER JOIN pg_catalog.pg_inherits AS i
			ON cl.oid = i.inhparent
		LEFT OUTER JOIN pg_catalog.pg_class AS cl2
			ON i.inhrelid = cl2.oid
			AND cl2.relispartition
			AND cl2.relkind = 'r'
		LEFT OUTER JOIN pg_catalog.pg_namespace AS n2
			ON cl2.relnamespace = n2.oid
		LEFT OUTER JOIN pg_catalog.pg_roles AS r2
			 ON cl2.relowner = r2.oid
		WHERE cl.relkind = 'p'
		;

		COMMENT ON VIEW pg_partitioned_tables_infos IS 'Creates a view to get information about partitioned tables';
		COMMENT ON COLUMN pg_partitioned_tables_infos.parent_relid IS 'The oid of the parent table.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.parent_schemaname IS 'The schema where the parent table is located.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.parent_tablename IS 'The name of the parent table.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.parent_owner IS 'The role name of the owner of the parent table.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.partition_strategy IS 'The partition strategy of the partitioned table, this can be LIST, RANGE, or HASH.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.count_of_partitions IS 'The number of partitions of the parent table.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.overall_size IS 'The overall size of the table including all partitions.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.child_relid IS 'The oid of the partition of a partitioned table.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.child_schemaname IS 'The name of the schema where the partition of a partitioned table is located.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.child_tablename IS 'The name of the partition of a partitioned table.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.child_owner IS 'The role name of the owner of the partition of a partitioned table.';
		COMMENT ON COLUMN pg_partitioned_tables_infos.child_size IS 'The size of this partition.';
	END IF;

END $$;

/**
 * Creates a view to get all connections and their locks.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */
 CREATE OR REPLACE VIEW pg_active_locks AS
 SELECT DISTINCT pid
 	, state
 	, datname AS database_name
 	, usename AS rolename
 	, application_name
 	, client_addr AS client_address
 	, query_start
	, age (now(), a.query_start) AS query_age
 	, wait_event_type
 	, wait_event
 	, locktype
 	, mode
 	, query
 FROM pg_stat_activity AS a
 INNER JOIN pg_locks AS l
 	USING(pid)
 ;

COMMENT ON VIEW pg_active_locks IS 'Creates a view to get all connections and their locks';
COMMENT ON COLUMN pg_active_locks.pid IS 'The process id of the backend, needed in case a connection should be terminated with the function pg-terminate_backend(pid). For details see [SERVER SIGNALING FUNCTIONS](https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SIGNAL).';
COMMENT ON COLUMN pg_active_locks.state IS 'The current state of the connection.';
COMMENT ON COLUMN pg_active_locks.database_name IS 'The database name in which the queries have been executed.';
COMMENT ON COLUMN pg_active_locks.rolename IS 'The role/user who executed the queries.';
COMMENT ON COLUMN pg_active_locks.application_name IS 'The name of the application, could be empty if not set by a client.';
COMMENT ON COLUMN pg_active_locks.client_address IS 'The clients IP address.';
COMMENT ON COLUMN pg_active_locks.query_start IS 'The timestamp of the start of the query.';
COMMENT ON COLUMN pg_active_locks.query_age IS 'How long the query is already running.';
COMMENT ON COLUMN pg_active_locks.wait_event_type IS 'The type of event for which the backend is waiting, if any; otherwise NULL, for details see [WAIT EVENT TABLE](https://www.postgresql.org/docs/current/monitoring-pg_html#WAIT-EVENT-TABLE).';
COMMENT ON COLUMN pg_active_locks.wait_event IS 'Wait event name if backend is currently waiting, otherwise NULL. For details see [WAIT EVENT ACTIVITY TABLE](https://www.postgresql.org/docs/current/monitoring-pg_html#WAIT-EVENT-ACTIVITY-TABLE) and [WAIT EVENT TIMEOUT TABLE](https://www.postgresql.org/docs/current/monitoring-pg_html#WAIT-EVENT-TIMEOUT-TABLE)';
COMMENT ON COLUMN pg_active_locks.locktype IS 'The type of the lockable object: relation, extend, frozenid, page, tuple, transactionid, virtualxid, spectoken, object, userlock, advisory, or applytransaction, see [Wait Events of Type Lock](https://www.postgresql.org/docs/current/monitoring-stats.html#WAIT-EVENT-LOCK-TABLE).';
COMMENT ON COLUMN pg_active_locks.mode IS 'The name of the lock mode held or desired by this process. For details see [TABLE LEVEL LOCKS](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-TABLES) and [SERIALIZABLE ISOLATION LEVEL](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-SERIALIZABLE)';
COMMENT ON COLUMN pg_active_locks.query IS 'This is the SQL source of the query.';

/**
 * Creates a view to get the ownership of all objects in the current database.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW pg_object_ownership AS
WITH dbobjects AS
	(
		SELECT cls.oid
			, nsp.nspname AS object_schema
			, cls.relname AS object_name
			, rol.rolname AS owner
			, CASE cls.relkind
				WHEN 'I' THEN
					'PARTITIONED INDEX'
				WHEN 'S' THEN
					'SEQUENCE'
				WHEN 'c' THEN
					'COMPOSITE TYPE'
				WHEN 'f' THEN
					'FOREIGN TABLE'
				WHEN 'i' THEN
					'INDEX'
				WHEN 'm' THEN
					'MATERIALIZED_VIEW'
				WHEN 'p' THEN
					'PARTITIONED TABLE'
				WHEN 'r' THEN
					'TABLE'
				WHEN 'v' THEN
					'VIEW'
				ELSE
					cls.relkind::text
			END AS object_type
		FROM pg_catalog.pg_class AS cls
		INNER JOIN pg_roles AS rol
			ON cls.relowner = rol.oid
		INNER JOIN pg_catalog.pg_namespace AS nsp
			ON cls.relnamespace = nsp.oid
		UNION ALL
		SELECT db.oid
			, NULL AS object_schema
			, db.datname AS object_name
			, rol.rolname AS owner
			, 'DATABASE' AS object_type
		FROM pg_catalog.pg_database AS db
		INNER JOIN pg_roles AS rol
			ON db.datdba = rol.oid
		UNION ALL
		SELECT ext.oid
			, NULL AS object_schema
			, ext.extname
			, rol.rolname AS owner
			, 'EXTENSION' AS object_type
		FROM pg_catalog.pg_extension AS ext
		INNER JOIN pg_roles AS rol
			ON ext.extowner = rol.oid
		UNION ALL
		SELECT fdw.oid
			, NULL AS object_schema
			, fdw.fdwname AS object_name
			, rol.rolname AS owner
			, 'FOREIGN DATA WRAPPER' AS object_type
		FROM pg_catalog.pg_foreign_data_wrapper AS fdw
		INNER JOIN pg_roles AS rol
			ON fdw.fdwowner = rol.oid
		UNION ALL
		SELECT srv.oid
			, NULL AS object_schema
			, srv.srvname AS object_name
			, rol.rolname AS owner
			, 'FOREIGN SERVER' AS object_type
		FROM pg_catalog.pg_foreign_server AS srv
		INNER JOIN pg_roles AS rol
			ON srv.srvowner = rol.oid
		UNION ALL
		SELECT lang.oid
			, NULL AS object_schema
			, lang.lanname AS object_name
			, rol.rolname AS owner
			, 'LANGUAGE' AS object_type
		FROM pg_catalog.pg_language AS lang
		INNER JOIN pg_roles AS rol
			ON lang.lanowner = rol.oid
		UNION ALL
		SELECT nsp.oid
			, NULL AS object_schema
			, nsp.nspname AS object_name
			, rol.rolname AS owner
			, 'SCHEMA' AS object_type
		FROM pg_catalog.pg_namespace AS nsp
		INNER JOIN pg_roles AS rol
			ON nsp.nspowner = rol.oid
		UNION ALL
		SELECT opc.oid
			, NULL AS object_schema
			, opc.opcname AS object_name
			, rol.rolname AS owner
			, 'OPERATOR CLASS' AS object_type
		FROM pg_catalog.pg_opclass AS opc
		INNER JOIN pg_roles AS rol
			ON opc.opcowner = rol.oid
		UNION ALL
		SELECT pro.oid
			, nsp.nspname AS object_schema
			, pro.proname AS object_name
			, rol.rolname AS owner
			, CASE lower(pro.prokind)
				WHEN 'f' THEN
					'FUNCTION'
				WHEN 'p' THEN
					'PROCEDURE'
				WHEN 'a' THEN
					'AGGREGATE FUNCTION'
				WHEN 'w' THEN
					'WINDOW FUNCTION'
				ELSE
					lower(pro.prokind)
			END AS object_type
		FROM pg_catalog.pg_proc AS pro
		INNER JOIN pg_roles AS rol
			ON pro.proowner = rol.oid
		INNER JOIN pg_catalog.pg_namespace nsp
			ON pro.pronamespace = nsp.oid
			WHERE nsp.nspname NOT IN ('pg_catalog', 'information_schema')
		UNION ALL
		SELECT col.oid
			, NULL AS object_schema
			, col.collname AS object_name
			, rol.rolname AS owner
			, 'COLLATION' AS object_type
		FROM pg_catalog.pg_collation AS col
		INNER JOIN pg_roles AS rol
			ON col.collowner = rol.oid
		UNION ALL
		SELECT con.oid
			, NULL AS object_schema
			, con.conname AS object_name
			, rol.rolname AS owner
			, 'CONVERSION' AS object_type
		FROM pg_catalog.pg_conversion AS con
		INNER JOIN pg_roles AS rol
			ON con.conowner = rol.oid
		UNION ALL
		SELECT evt.oid
			, NULL AS object_schema
			, evt.evtname AS object_name
			, rol.rolname AS owner
			, 'EVENT TRIGGER' AS object_type
		FROM pg_catalog.pg_event_trigger AS evt
		INNER JOIN pg_roles AS rol
			ON evt.evtowner = rol.oid
		UNION ALL
		SELECT opf.oid
			, NULL AS object_schema
			, opf.opfname AS object_name
			, rol.rolname AS owner
			, 'OPERATION FAMILY' AS object_type
		FROM pg_catalog.pg_opfamily AS opf
		INNER JOIN pg_roles AS rol
			ON opf.opfowner = rol.oid
		UNION ALL
		SELECT pub.oid
			, NULL AS object_schema
			, pub.pubname AS object_name
			, rol.rolname AS owner
			, 'PUBLICATIONS' AS object_type
		FROM pg_catalog.pg_publication AS pub
		INNER JOIN pg_roles AS rol
			ON pub.pubowner = rol.oid
	)
SELECT dbobjects.oid
	, dbobjects.object_schema
	, dbobjects.owner
	, dbobjects.object_type
	, depend.deptype
	, CASE depend.deptype
		WHEN 'n' THEN
			'DEPENDENCY_NORMAL'
		WHEN 'a' THEN
			'DEPENDENCY_AUTO'
		WHEN 'i' THEN
			'DEPENDENCY_INTERNAL'
		WHEN 'P' THEN
			'DEPENDENCY_PARTITION_PRI'
		WHEN 'S' THEN
			'DEPENDENCY_PARTITION_SEC'
		WHEN 'e' THEN
			'DEPENDENCY_EXTENSION'
		WHEN 'x' THEN
			'DEPENDENCY_EXTENSION'
		WHEN 'p' THEN
			'DEPENDENCY_PIN'
		ELSE
			'NOT DEFINED, SEE DOCUMENTATION'
	END AS dependency_type
FROM dbobjects
LEFT OUTER JOIN pg_catalog.pg_depend AS depend
	ON dbobjects.oid = depend.objid
WHERE object_schema NOT IN ('information_schema', 'pg_catalog')
	AND object_schema NOT LIKE 'pg_toast%'
;

-- Add a comment
COMMENT ON VIEW pg_object_ownership IS 'The view returns all objects, its type, and its ownership in the current database, excluding those in the schema pg_catalog and information_schema';
COMMENT ON VIEW pg_object_ownership IS 'The view returns all objects, its type, and its ownership in the current database, excluding those in the schema pg_catalog and information_schema';
COMMENT ON COLUMN pg_object_ownership.oid IS 'The object identifier of the object.';
COMMENT ON COLUMN pg_object_ownership.object_schema IS 'The schema name where the object is stored in.';
COMMENT ON COLUMN pg_object_ownership.owner IS 'The role name that owns the object.';
COMMENT ON COLUMN pg_object_ownership.object_type IS 'The type of object, for example TABLE, INDEX, etc.';
COMMENT ON COLUMN pg_object_ownership.deptype IS 'The dependency shortcut, see dependency_type for further description.';
COMMENT ON COLUMN pg_object_ownership.dependency_type IS 'The description of the deptype: n = DEPENDENCY_NORMAL, a = DEPENDENCY_AUTO, i = DEPENDENCY_INTERNAL, P = DEPENDENCY_PARTITION_PRI, S = DEPENDENCY_PARTITION_SEC, e = DEPENDENCY_EXTENSION, p = DEPENDENCY_PIN, others: NOT DEFINED, SEE DOCUMENTATION.';

/**
 * The view shows bloat in tables and indexes.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */
CREATE OR REPLACE VIEW pg_bloat_info AS
WITH constants AS
	(
		SELECT current_setting('block_size')::numeric AS bs
			, 23 AS hdr
			, 4 AS ma
	)
, bloat_info AS
	(
		SELECT ma
			,bs
			,schemaname
			,tablename
			, (datawidth + (hdr + ma - (case when hdr%ma = 0 THEN ma ELSE hdr%ma END)))::numeric AS datahdr
			, (maxfracsum * (nullhdr + ma - (case when nullhdr%ma = 0 THEN ma ELSE nullhdr%ma END))) AS nullhdr2
		FROM
			(
				SELECT schemaname
					, tablename
					, hdr
					, ma
					, bs
					, sum ((1 - null_frac) * avg_width) AS datawidth
					, max (null_frac) AS maxfracsum
					, hdr +
						(
        						SELECT 1 + count(*) / 8
							FROM pg_stats s2
							WHERE null_frac != 0
							AND s2.schemaname = s.schemaname
							AND s2.tablename = s.tablename
						) AS nullhdr
				FROM pg_stats AS s, constants
				GROUP BY 1
					,2
					,3
					,4
					,5
			) AS foo
	)
, table_bloat AS
	(
		SELECT schemaname
			, tablename
			, cc.relpages
			, bs
			, CEIL ((cc.reltuples * ((datahdr + ma - (CASE WHEN datahdr%ma = 0 THEN ma ELSE datahdr%ma END)) + nullhdr2 + 4)) / (bs - 20::float)) AS otta
		FROM bloat_info
		INNER JOIN pg_class cc
			ON cc.relname = bloat_info.tablename
		INNER JOIN pg_namespace nn
			ON cc.relnamespace = nn.oid
			AND nn.nspname = bloat_info.schemaname
			AND nn.nspname NOT IN
				(
					'information_schema',
					'pg_catalog'
				)
	)
, index_bloat AS
	(
		SELECT schemaname
			, tablename
			, bs
			, COALESCE (c2.relname,'?') AS iname
			, COALESCE (c2.reltuples,0) AS ituples
			, COALESCE (c2.relpages,0) AS ipages
			, COALESCE (CEIL ((c2.reltuples * (datahdr - 12)) / (bs - 20::float)), 0) AS iotta -- very rough approximation, assumes all cols
		FROM bloat_info
		INNER JOIN pg_class AS cc
			ON cc.relname = bloat_info.tablename
		INNER JOIN pg_namespace AS nn
			ON cc.relnamespace = nn.oid
			AND nn.nspname = bloat_info.schemaname
			AND nn.nspname NOT IN
				(
					'information_schema',
					'pg_catalog'
				)
		INNER JOIN pg_index AS i
			ON indrelid = cc.oid
		INNER JOIN pg_class AS c2
			ON c2.oid = i.indexrelid
	)
SELECT type
	, schemaname
	, object_name
	, bloat
	, pg_size_pretty(raw_waste) AS waste
FROM
	(
		SELECT 'table' AS type
			, schemaname
			, tablename AS object_name
			, ROUND (CASE WHEN otta = 0 THEN 0.0 ELSE table_bloat.relpages/otta::numeric END, 1) AS bloat
			, CASE WHEN relpages < otta THEN '0' ELSE (bs * (table_bloat.relpages-otta)::bigint)::bigint END AS raw_waste
		FROM table_bloat
		UNION
		SELECT 'index' AS type
			, schemaname
			, tablename || '::' || iname as object_name
			, ROUND (CASE WHEN iotta = 0 OR ipages = 0 THEN 0.0 ELSE ipages / iotta::numeric END, 1) AS bloat
			, CASE WHEN ipages < iotta THEN '0' ELSE (bs*(ipages-iotta))::bigint END AS raw_waste
		FROM index_bloat
	) AS bloat_summary
ORDER BY raw_waste DESC
	, bloat DESC
;

COMMENT ON VIEW pg_bloat_info IS 'The view shows bloat in tables and indexes.';
COMMENT ON COLUMN pg_bloat_info.type IS 'Object type, can be table or index.';
COMMENT ON COLUMN pg_bloat_info.schemaname IS 'The schema where the object is located.';
COMMENT ON COLUMN pg_bloat_info.object_name IS 'The name of the table or index.';
COMMENT ON COLUMN pg_bloat_info.bloat IS 'The estimated bloat ratio.';
COMMENT ON COLUMN pg_bloat_info.waste IS 'The estimated wasted disk spae with unit (kB, MB, GB).';

/**
 * The view shows unused indexes with further information about the table.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW pg_unused_indexes AS
SELECT schemaname AS schema_name
	, relname AS table_name
	, indexrelname AS index_name
	, idx_scan
	, pg_size_pretty (pg_table_size ('"' || schemaname || '"."' || relname || '"')) AS table_size
	, pg_size_pretty (pg_total_relation_size ('"' || schemaname || '"."' || relname || '"')) AS table_total_size
	, pg_size_pretty (pg_indexes_size ('"' || schemaname || '"."' || relname || '"')) AS all_indexes_size
	, pg_size_pretty (pg_relation_size (indexrelid)) AS index_size
	, pg_size_pretty (sum (pg_relation_size (indexrelid)) over ()) AS  size_of_all_indexes
FROM pg_stat_all_indexes
WHERE idx_scan = 0
AND schemaname NOT IN
	(
		'information_schema',
		'pg_catalog',
		'pg_toast'
	)
;

COMMENT ON VIEW pg_unused_indexes IS 'The view shows unused indexes with further information about the table.';
COMMENT ON COLUMN pg_unused_indexes.schema_name IS 'The name of the schema.';
COMMENT ON COLUMN pg_unused_indexes.table_name IS 'The name of the table.';
COMMENT ON COLUMN pg_unused_indexes.index_name IS 'The name of the index';
COMMENT ON COLUMN pg_unused_indexes.idx_scan IS 'The number of query executions where this index has been used, is always zero.';
COMMENT ON COLUMN pg_unused_indexes.table_size IS 'The size of the table without indexes.';
COMMENT ON COLUMN pg_unused_indexes.table_total_size IS 'The size of the table including all indexes.';
COMMENT ON COLUMN pg_unused_indexes.all_indexes_size IS 'The size of all indexes in this table.';
COMMENT ON COLUMN pg_unused_indexes.index_size IS 'The size of the index mentioned in index_name.';
COMMENT ON COLUMN pg_unused_indexes.size_of_all_indexes IS 'The size of all indexes over all tables.';

/**
 * Creates a view to get information about bloat in tables.
 * The view requires the extension pgstattuple to be installed.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) pgstattuple_exists
	FROM pg_extension
	WHERE extname = 'pgstattuple'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed
		view_source :=
$string$
CREATE OR REPLACE VIEW pg_table_bloat AS
WITH table_list AS
	(
		SELECT t.schemaname AS schema_name
			, t.tablename AS table_name
			, t.tableowner AS table_owner
			, t.schemaname || '.' || t.tablename AS schema_table_name
		FROM pg_catalog.pg_tables AS t
		LEFT OUTER JOIN pg_catalog.pg_partitioned_table AS pt
			ON (t.schemaname || '.' || t.tablename)::regclass::oid = pt.partrelid
		WHERE t.schemaname NOT IN
			(
				'pg_catalog',
				'information_schema'
			)
		AND pt.partrelid IS NULL
	)
SELECT table_list.schema_name
	, table_list.table_name
	, table_list.table_owner
	, st.table_len AS table_length_in_bytes
	, st.tuple_count
	, st.tuple_len AS tuple_length_in_bytes
	, st.tuple_percent
	, st.dead_tuple_count
	, st.dead_tuple_len AS dead_tuple_length_in_bytes
	, st.dead_tuple_percent
	, st.free_space
	, st.free_percent
FROM table_list
CROSS JOIN LATERAL pgstattuple(table_list.schema_table_name) AS st
ORDER BY schema_name , table_name
;
$string$
;
		EXECUTE view_source;

		COMMENT ON VIEW pg_table_bloat IS 'The list of tables and current bloat.';
		COMMENT ON COLUMN pg_table_bloat.schema_name IS 'The name of the schema.';
		COMMENT ON COLUMN pg_table_bloat.table_name IS 'The name of the table.';
		COMMENT ON COLUMN pg_table_bloat.table_owner IS 'The name of the role that owns the table.';
		COMMENT ON COLUMN pg_table_bloat.table_length_in_bytes IS 'The physical table size in bytes.';
		COMMENT ON COLUMN pg_table_bloat.tuple_count IS 'The number of live tuples.';
		COMMENT ON COLUMN pg_table_bloat.tuple_length_in_bytes IS 'The total length of live tuples in bytes.';
		COMMENT ON COLUMN pg_table_bloat.tuple_percent IS 'The percentage of live tuples compared to dead tuples.';
		COMMENT ON COLUMN pg_table_bloat.dead_tuple_count IS 'The number of dead tuples.';
		COMMENT ON COLUMN pg_table_bloat.dead_tuple_length_in_bytes IS 'The total length of dead tuples in bytes.';
		COMMENT ON COLUMN pg_table_bloat.dead_tuple_percent IS 'The percentage of dead tuples compared to live tuples.';
		COMMENT ON COLUMN pg_table_bloat.free_space IS 'The total free space in bytes of this table.';
		COMMENT ON COLUMN pg_table_bloat.free_percent IS 'The percentage of free space of this tuple compared to used space.';

	END IF;

END $$;

/**
 * The view tables with sequential scans, which probably are missing indexes
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

 DO $$
 DECLARE
 	version_greater_15 BOOLEAN;
 BEGIN
 	SELECT to_number((string_to_array(version(), ' '))[2], '999.99') >= 16 INTO version_greater_15;


 	IF version_greater_15 THEN
 		-- Create the view pg_functions for PostgreSQL 15 or newer
		CREATE OR REPLACE VIEW pg_missing_indexes AS
		SELECT t.schemaname AS schema_name
			, t.relname AS table_name
			, t.seq_scan AS sequential_scans
			, t.last_seq_scan AS last_sequential_scans
		FROM pg_stat_all_tables AS t
		WHERE schemaname NOT IN
			(
				'pg_toast',
				'pg_catalog',
				'information_schema'
			)
		AND t.seq_scan > 0
		;

		COMMENT ON VIEW pg_missing_indexes IS 'The view tables with sequential scans, which probably are missing indexes. The higher the number the more likely an index is might be needed.';
		COMMENT ON COLUMN pg_missing_indexes.schema_name IS 'The name of the schema.';
		COMMENT ON COLUMN pg_missing_indexes.table_name IS 'The name of the table.';
		COMMENT ON COLUMN pg_missing_indexes.sequential_scans IS 'Number of sequential scans on this table.';
		COMMENT ON COLUMN pg_missing_indexes.last_sequential_scans IS 'Last time of a sequential scan on this table.';
	ELSE
		-- Create the view pg_functions for PostgreSQL 15 or newer
		CREATE OR REPLACE VIEW pg_missing_indexes AS
		SELECT t.schemaname AS schema_name
			, t.relname AS table_name
			, t.seq_scan AS sequential_scans
		FROM pg_stat_all_tables AS t
		WHERE schemaname NOT IN
			(
				'pg_toast',
				'pg_catalog',
				'information_schema'
			)
		AND t.seq_scan > 0
		;

		COMMENT ON VIEW pg_missing_indexes IS 'The view tables with sequential scans, which probably are missing indexes. The higher the number the more likely an index is might be needed.';
		COMMENT ON COLUMN pg_missing_indexes.schema_name IS 'The name of the schema.';
		COMMENT ON COLUMN pg_missing_indexes.table_name IS 'The name of the table.';
		COMMENT ON COLUMN pg_missing_indexes.sequential_scans IS 'Number of sequential scans on this table.';
	END IF;
END $$;

/**
 * Creates a view to get information rights on database objects.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW pg_role_permissions AS
WITH rtg AS
	(
		SELECT rtg.grantee AS role_name
			, rtg.table_catalog AS database_name
			, rtg.table_schema  AS schema_name
			, rtg.table_name
			, array_agg(rtg.privilege_type) AS privilege
			, rtg.is_grantable
		FROM information_schema.role_table_grants AS rtg
		GROUP BY rtg.grantee
			, rtg.table_catalog
			, rtg.table_schema
			, rtg.table_name
			, rtg.is_grantable
	)
, rtg_res AS
	(
		SELECT rtg.role_name
			, rtg.database_name
			, rtg.schema_name
			, rtg.table_name AS object_name
			, 'TABLE' AS object_type
			, rtg.privilege
			, 'SELECT' = ANY (rtg.privilege) AS "SELECT"
			, 'INSERT' = ANY (rtg.privilege) AS "INSERT"
			, 'UPDATE' = ANY (rtg.privilege) AS "UPDATE"
			, 'DELETE' = ANY (rtg.privilege) AS "DELETE"
			, 'TRUNCATE' = ANY (rtg.privilege) AS "TRUNCATE"
			, 'REFERENCES' = ANY (rtg.privilege) AS "REFERENCES"
			, 'TRIGGER' = ANY (rtg.privilege) AS "TRIGGER"
			, NULL::boolean AS "USAGE"
			, NULL::boolean AS "EXECUTE"
			, rtg.is_grantable = 'YES' AS "GRANTABLE"
		FROM rtg
	)
,rug AS
	(
		SELECT rug.grantee AS role_name
			, rug.udt_catalog AS database_name
			, rug.udt_schema AS schema_name
			, rug.udt_name AS object_name
			, 'USER DEFINED TYPE' AS object_type
			, string_to_array(substring(rug.privilege_type, 6, 5), '.') AS privilege
			, rug.is_grantable
		FROM information_schema.role_udt_grants AS rug
	)
, rug_res AS
	(
		SELECT rug.role_name
			, rug.database_name
			, rug.schema_name
			, rug.object_name
			, rug.object_type
			, rug.privilege
			, NULL::boolean AS "SELECT"
			, NULL::boolean AS "INSERT"
			, NULL::boolean AS "UPDATE"
			, NULL::boolean AS "DELETE"
			, NULL::boolean AS "TRUNCATE"
			, NULL::boolean AS "REFERENCES"
			, NULL::boolean AS "TRIGGER"
			, 'USAGE' = ANY (rug.privilege) AS "USAGE"
			, NULL::boolean AS "EXECUTE"
			, rug.is_grantable = 'YES' AS "GRANTABLE"
		FROM rug
	)
, rrg AS
	(
		SELECT rrg.grantee AS role_name
			, rrg.routine_catalog AS database_name
			, rrg.routine_schema AS schema_name
			, rrg.routine_name AS object_name
			, 'FUNCTION/PROCEDURE' AS object_type
			, string_to_array(rrg.privilege_type, '.') AS privilege
			, rrg.is_grantable
		FROM information_schema.role_routine_grants AS rrg
	)
, rrg_res AS
	(
		SELECT rrg.role_name
			, rrg.database_name
			, rrg.schema_name
			, rrg.object_name
			, rrg.object_type
			, rrg.privilege
			, NULL::boolean AS "SELECT"
			, NULL::boolean AS "INSERT"
			, NULL::boolean AS "UPDATE"
			, NULL::boolean AS "DELETE"
			, NULL::boolean AS "TRUNCATE"
			, NULL::boolean AS "REFERENCES"
			, NULL::boolean AS "TRIGGER"
			, NULL::boolean AS "USAGE"
			, 'EXECUTE' = ANY (rrg.privilege) AS "EXECUTE"
			, rrg.is_grantable = 'YES' AS "GRANTABLE"
		FROM rrg
	)
, rcg AS
	(
		SELECT rcg.grantee AS role_name
			, rcg.table_catalog AS database_name
			, rcg.table_schema AS schema_name
			, rcg.table_name || '.' || rcg.column_name AS object_name
			, 'TABLE COLUMN' AS object_type
			, array_agg(rcg.privilege_type) AS privilege
			, rcg.is_grantable
		FROM information_schema.role_column_grants AS rcg
		GROUP BY rcg.grantee
			, rcg.table_catalog
			, rcg.table_schema
			, rcg.table_name
			, rcg.column_name
			, rcg.is_grantable
	)
, rcg_res AS
	(
		SELECT rcg.role_name
			, rcg.database_name
			, rcg.schema_name
			, rcg.object_name
			, 'TABLE' AS object_type
			, rcg.privilege
			, 'SELECT' = ANY (rcg.privilege) AS "SELECT"
			, 'INSERT' = ANY (rcg.privilege) AS "INSERT"
			, 'UPDATE' = ANY (rcg.privilege) AS "UPDATE"
			, NULL::boolean AS "DELETE"
			, NULL::boolean AS "TRUNCATE"
			, 'REFERENCES' = ANY (rcg.privilege) AS "REFERENCES"
			, NULL::boolean AS "TRIGGER"
			, NULL::boolean AS "USAGE"
			, NULL::boolean AS "EXECUTE"
			, rcg.is_grantable = 'YES' AS "GRANTABLE"
		FROM rcg
	)
, res AS
	(
		SELECT *
		FROM rtg_res
		UNION ALL
		SELECT *
		FROM rug_res
		UNION ALL
		SELECT *
		FROM rrg_res
		UNION ALL
		SELECT *
		FROM rcg_res
	)
SELECT *
FROM res
WHERE res.schema_name NOT IN
	(
		'information_schema',
		'pg_catalog'
	)
;

COMMENT ON VIEW pg_role_permissions IS 'Information about rights of roles on database objects.';
COMMENT ON COLUMN pg_role_permissions.role_name IS 'The name of the role.';
COMMENT ON COLUMN pg_role_permissions.database_name IS 'The name of the database.';
COMMENT ON COLUMN pg_role_permissions.schema_name IS 'The name of the schema.';
COMMENT ON COLUMN pg_role_permissions.object_name IS 'The name of the object.';
COMMENT ON COLUMN pg_role_permissions.object_type IS 'The type of the object, can be one of TABLE, FUNCTION/PROCEDURE, USER DEFINED TYPE, or TABLE COLUMN.';
COMMENT ON COLUMN pg_role_permissions.privilege IS 'All privileges as an array.';
COMMENT ON COLUMN pg_role_permissions."SELECT" IS 'SELECT right, NULL if not supported by the object.';
COMMENT ON COLUMN pg_role_permissions."INSERT" IS 'INSERT right, NULL if not supported by the object.';
COMMENT ON COLUMN pg_role_permissions."UPDATE" IS 'UPDATE right, NULL if not supported by the object.';
COMMENT ON COLUMN pg_role_permissions."DELETE" IS 'DELETE right, NULL if not supported by the object.';
COMMENT ON COLUMN pg_role_permissions."TRUNCATE" IS 'TRUNCATE right, NULL if not supported by the object.';
COMMENT ON COLUMN pg_role_permissions."REFERENCES" IS 'REFERENCES right, NULL if not supported by the object.';
COMMENT ON COLUMN pg_role_permissions."TRIGGER" IS 'TRIGGER right, NULL if not supported by the object.';
COMMENT ON COLUMN pg_role_permissions."USAGE" IS 'USAGE right, NULL if not supported by the object.';
COMMENT ON COLUMN pg_role_permissions."EXECUTE" IS 'EXECUTE right, NULL if not supported by the object.';
COMMENT ON COLUMN pg_role_permissions."GRANTABLE" IS 'GRANTABLE right, role can grant rights on this object.';

/**
 * Role infos based on pg_roles and role_inheritance.
 * NOTE: The function role_inheritance must exist!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	function_exists BOOLEAN;
 	version_greater_15 BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) = 1 AS function_exists
	INTO function_exists
	FROM pg_catalog.pg_proc AS f
	WHERE f.proname = 'role_inheritance'
		AND f.pronargs = 1
	;

	IF function_exists THEN
		-- The view is only created when the function role_inheritance
	 	SELECT to_number((string_to_array(version(), ' '))[2], '999.99') >= 16
		INTO version_greater_15;
	
	 	IF version_greater_15 THEN
	 		-- Create the view for PostgreSQL 16 or newer
			view_source :=
$string$
CREATE OR REPLACE VIEW pg_role_infos AS
SELECT pr.oid
	, pr.rolname AS rolename
	, pr.rolsuper
	, pr.rolcreaterole
	, pr.rolcreatedb
	, pr.rolreplication
	, pr.rolcanlogin
	, pr.rolvaliduntil
	, pr.rolconfig
	, ri.steps
	, ri.role_path
	, ri.inherit_option
FROM pg_catalog.pg_roles AS pr
, role_inheritance(pr.rolname) AS ri
;
$string$
;
			EXECUTE view_source;

			COMMENT ON COLUMN pg_role_infos.inherit_option IS 'True if the member can SET ROLE to the granted role.';
		ELSE
			-- Create the view for PostgreSQL older than 16
			view_source :=
$string$
CREATE OR REPLACE VIEW pg_role_infos AS
SELECT pr.oid
	, pr.rolname AS rolename
	, pr.rolsuper
	, pr.rolcreaterole
	, pr.rolcreatedb
	, pr.rolreplication
	, pr.rolcanlogin
	, pr.rolvaliduntil
	, pr.rolconfig
	, ri.steps
	, ri.role_path
FROM pg_catalog.pg_roles AS pr
, role_inheritance(pr.rolname) AS ri
;
$string$
;
			EXECUTE view_source;
		END IF;

		COMMENT ON VIEW pg_role_infos IS 'Role infos based on pg_roles and role_inheritance.';
		COMMENT ON COLUMN pg_role_infos.oid IS 'The object identifier of the role.';
		COMMENT ON COLUMN pg_role_infos.rolename IS 'The name of the role.';
		COMMENT ON COLUMN pg_role_infos.rolsuper IS 'The role has superuser privileges.';
		COMMENT ON COLUMN pg_role_infos.rolcreaterole IS 'The role can create more roles.';
		COMMENT ON COLUMN pg_role_infos.rolcreatedb IS 'The role can create databases.';
		COMMENT ON COLUMN pg_role_infos.rolreplication IS 'The role is a replication role. A replication role can initiate replication connections and create and drop replication slots.';
		COMMENT ON COLUMN pg_role_infos.rolcanlogin IS 'The role can log in. Usually group roles cannot login.';
		COMMENT ON COLUMN pg_role_infos.rolvaliduntil IS 'The password expiry time, NULL if no expiration.';
		COMMENT ON COLUMN pg_role_infos.rolconfig IS 'The role specific defaults for run-time configuration variables, for example the search path.';
		COMMENT ON COLUMN pg_role_infos.steps IS 'The order im which the inheritance is going.';
		COMMENT ON COLUMN pg_role_infos.role_path IS 'The path of the inheritance.';

	END IF;

END $$;

/**
 * Top ten queries with statistics about
 * - shared buffer read/write/dirty
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) AS pg_stat_statements_exists
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed
		view_source :=
$string$
CREATE OR REPLACE VIEW statistics_top_ten_query_times AS
SELECT queryid
	, query
	, round (total_exec_time::NUMERIC, 2) AS total_time
	, calls
	, round (mean_exec_time::NUMERIC, 2) AS mean_time
	, round
		(
			(
				100 * total_exec_time /
				sum(total_exec_time::numeric)
				OVER ()
			)::NUMERIC, 2
		) AS percentage_of_total_time
	, shared_blks_hit AS shared_buffer_hit
	, shared_blks_read AS shared_buffer_read
	, shared_blks_written AS shared_buffer_written
	, shared_blks_dirtied AS shared_buffer_dirty
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10
;
$string$
;
		EXECUTE view_source;

		COMMENT ON VIEW statistics_top_ten_query_times IS 'Top ten queries in execution times.';
		COMMENT ON COLUMN statistics_top_ten_query_times.queryid IS 'Query identifier, can be used to join the view with pg_stat_statements to get more information';
		COMMENT ON COLUMN statistics_top_ten_query_times.query IS 'This is the SQL source of the query.';
		COMMENT ON COLUMN statistics_top_ten_query_times.total_time IS 'This is the total execution time of the query in milliseconds.';
		COMMENT ON COLUMN statistics_top_ten_query_times.mean_time IS 'This is the average execution time of the query in milliseconds.';
		COMMENT ON COLUMN statistics_top_ten_query_times.calls IS 'This is many times a statement has been executed.';
		COMMENT ON COLUMN statistics_top_ten_query_times.percentage_of_total_time IS 'Percentage of execution time of all executed queries.';
		COMMENT ON COLUMN statistics_top_ten_query_times.shared_buffer_hit IS 'This is the count of times disk blocks were found already cached in memory (no I/O was needed).';
		COMMENT ON COLUMN statistics_top_ten_query_times.shared_buffer_read IS 'This is the read is the count of times disk blocks had to be read into memory, which indicates actual I/O operations. High values in shared_blks_read suggest that these queries are the most I/O intensive, which can be a starting point for performance optimization.';
		COMMENT ON COLUMN statistics_top_ten_query_times.shared_buffer_written IS 'This is the number of shared blocks written into shared buffer and written to disk.';
		COMMENT ON COLUMN statistics_top_ten_query_times.shared_buffer_dirty IS 'This is the number of shared blocks "dirtied" by the query. Dirteid means the number of blocks where at least one tuple got modified and hat ot be written to disk.';

	END IF;

END $$;

/**
 * Top ten queries with high i/o activity
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
	version_greater_17 BOOLEAN;
BEGIN

	SELECT count(*) AS pg_extension_installed
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		SELECT to_number((string_to_array(version(), ' '))[2], '999.99') >= 17 INTO version_greater_17;

	  IF version_greater_17 THEN
			-- The view is only created when pgstattuple is installed
			CREATE OR REPLACE VIEW statistics_top_ten_query_average_time_in_seconds AS
			SELECT s.userid::regrole AS rolename
				, d.datname AS database_name
				, s.query
				, s.mean_exec_time/1000 AS mean_time_seconds
			FROM pg_stat_statements AS s
			INNER JOIN pg_catalog.pg_database AS d
				ON s.dbid = d.oid
			ORDER BY (s.shared_blk_read_time + s.shared_blk_write_time) DESC
			LIMIT 10
			;
		ELSE
			-- The view is only created when pgstattuple is installed
			CREATE OR REPLACE VIEW statistics_top_ten_query_average_time_in_seconds AS
			SELECT s.userid::regrole AS rolename
				, d.datname AS database_name
				, s.query
				, s.mean_exec_time/1000 AS mean_time_seconds
			FROM pg_stat_statements AS s
			INNER JOIN pg_catalog.pg_database AS d
				ON s.dbid = d.oid
			ORDER BY s.mean_exec_time DESC
			LIMIT 10
			;
		END IF;

		COMMENT ON VIEW statistics_top_ten_query_average_time_in_seconds IS 'Top ten queries with high i/o activity with the average execution time in seconds.';
		COMMENT ON COLUMN statistics_top_ten_query_average_time_in_seconds.rolename IS 'The role/user who executed the query.';
		COMMENT ON COLUMN statistics_top_ten_query_average_time_in_seconds.database_name IS 'The database name in which the query has been executed.';
		COMMENT ON COLUMN statistics_top_ten_query_average_time_in_seconds.query IS 'This is the SQL source of the query.';
		COMMENT ON COLUMN statistics_top_ten_query_average_time_in_seconds.mean_time_seconds IS 'The average execution time of the query in seconds';

	END IF;

END $$;

/**
 * Top ten time consuming queries
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) AS pg_stat_statements_exists
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed
		view_source :=
$string$
CREATE OR REPLACE VIEW statistics_top_ten_time_consuming_queries AS
SELECT s.userid::regrole AS rolename
	, d.datname AS database_name
	, query
	, calls
	, total_exec_time/1000 as total_time_seconds
	, min_exec_time/1000 as min_time_seconds
	, max_exec_time/1000 as max_time_seconds
	, mean_exec_time/1000 as mean_time_seconds
FROM pg_stat_statements AS s
INNER JOIN pg_catalog.pg_database AS d
	ON s.dbid = d.oid
ORDER BY mean_exec_time DESC
LIMIT 10
;
$string$
;
		EXECUTE view_source;

		COMMENT ON VIEW statistics_top_ten_time_consuming_queries IS 'Top ten time consuming queries.';
		COMMENT ON COLUMN statistics_top_ten_time_consuming_queries.rolename IS 'The role/user who executed the query.';
		COMMENT ON COLUMN statistics_top_ten_time_consuming_queries.database_name IS 'The database name in which the query has been executed.';
		COMMENT ON COLUMN statistics_top_ten_time_consuming_queries.query IS 'This is the SQL source of the query.';
		COMMENT ON COLUMN statistics_top_ten_time_consuming_queries.calls IS 'The number of times the statement was executed.';
		COMMENT ON COLUMN statistics_top_ten_time_consuming_queries.total_time_seconds IS 'The total execution time in seconds.';
		COMMENT ON COLUMN statistics_top_ten_time_consuming_queries.min_time_seconds IS 'The minimum execution time in seconds.';
		COMMENT ON COLUMN statistics_top_ten_time_consuming_queries.max_time_seconds IS 'The maximum execution time in seconds.';
		COMMENT ON COLUMN statistics_top_ten_time_consuming_queries.mean_time_seconds IS 'The average execution time of the query in seconds';

	END IF;

END $$;

/**
 * Top ten queries with high memory usage
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) AS pg_stat_statements_exists
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed
		view_source :=
$string$
CREATE OR REPLACE VIEW statistics_top_ten_memory_usage_queries AS
SELECT s.userid::regrole AS rolename
	, d.datname AS database_name
	, s.query
	, (s.shared_blks_hit + s.shared_blks_dirtied) AS memory_hit_dirty
FROM pg_stat_statements AS s
INNER JOIN pg_catalog.pg_database AS d
	ON s.dbid = d.oid
ORDER BY (s.shared_blks_hit + s.shared_blks_dirtied) DESC
LIMIT 10
;
$string$
;
		EXECUTE view_source;

		COMMENT ON VIEW statistics_top_ten_memory_usage_queries IS 'Top ten queries with high memory usage.';
		COMMENT ON COLUMN statistics_top_ten_memory_usage_queries.rolename IS 'The role/user who executed the query.';
		COMMENT ON COLUMN statistics_top_ten_memory_usage_queries.database_name IS 'The database name in which the query has been executed.';
		COMMENT ON COLUMN statistics_top_ten_memory_usage_queries.query IS 'This is the SQL source of the query.';
		COMMENT ON COLUMN statistics_top_ten_memory_usage_queries.memory_hit_dirty IS 'This the sum of shared_blks_hit + s.shared_blks_dirtied, the calculated memory usage in byte.';

	END IF;

END $$;

/**
 * Top ten queries with high memory usage
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) AS pg_stat_statements_exists
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed
		view_source :=
$string$
CREATE OR REPLACE VIEW statistics_top_ten_called_queries AS
SELECT s.userid::regrole AS rolename
	, d.datname AS database_name
	, s.calls
	, s.query
FROM pg_stat_statements AS s
INNER JOIN pg_catalog.pg_database AS d
	ON s.dbid = d.oid
ORDER BY s.calls DESC
LIMIT 10
;
$string$
;
		EXECUTE view_source;

		COMMENT ON VIEW statistics_top_ten_called_queries IS 'Top ten queries by calls.';
		COMMENT ON COLUMN statistics_top_ten_called_queries.rolename IS 'The role/user who executed the query.';
		COMMENT ON COLUMN statistics_top_ten_called_queries.database_name IS 'The database name in which the query has been executed.';
		COMMENT ON COLUMN statistics_top_ten_called_queries.calls IS 'This is many times a statement has been executed.';
		COMMENT ON COLUMN statistics_top_ten_called_queries.query IS 'This is the SQL source of the query.';

	END IF;

END $$;

/**
 * Top ten queries by rows returned
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) AS pg_stat_statements_exists
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed
		view_source :=
$string$
CREATE OR REPLACE VIEW statistics_top_ten_rows_returned_queries AS
SELECT s.userid::regrole AS rolename
	, d.datname AS database_name
	, s.rows
	, s.query
FROM pg_stat_statements AS s
INNER JOIN pg_catalog.pg_database AS d
	ON s.dbid = d.oid
ORDER BY s.rows DESC
LIMIT 10
;
$string$
;
		EXECUTE view_source;

		COMMENT ON VIEW statistics_top_ten_rows_returned_queries IS 'Top ten queries by rows returned.';
		COMMENT ON COLUMN statistics_top_ten_rows_returned_queries.rolename IS 'The role/user who executed the query.';
		COMMENT ON COLUMN statistics_top_ten_rows_returned_queries.database_name IS 'The database name in which the query has been executed.';
		COMMENT ON COLUMN statistics_top_ten_rows_returned_queries.rows IS 'This is the count of the effected rows by this query.';
		COMMENT ON COLUMN statistics_top_ten_rows_returned_queries.query IS 'This is the SQL source of the query.';

	END IF;

END $$;

/**
 * Top ten queries by shared block hits
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) AS pg_stat_statements_exists
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed
		view_source :=
$string$
CREATE OR REPLACE VIEW statistics_top_ten_shared_block_hits_queries AS
SELECT s.userid::regrole AS rolename
	, d.datname AS database_name
	, s.shared_blks_hit AS shared_buffer_hit
	, s.shared_blks_read AS shared_buffer_read
	, s.query
FROM pg_stat_statements AS s
INNER JOIN pg_catalog.pg_database AS d
	ON s.dbid = d.oid
ORDER BY s.shared_blks_hit DESC
LIMIT 10
;
$string$
;
		EXECUTE view_source;

		COMMENT ON VIEW statistics_top_ten_shared_block_hits_queries IS 'Top ten queries by shared block hits.';
		COMMENT ON COLUMN statistics_top_ten_shared_block_hits_queries.rolename IS 'The role/user who executed the query.';
		COMMENT ON COLUMN statistics_top_ten_shared_block_hits_queries.database_name IS 'The database name in which the query has been executed.';
		COMMENT ON COLUMN statistics_top_ten_shared_block_hits_queries.shared_buffer_hit IS 'This is the count of times disk blocks were found already cached in memory (no I/O was needed).';
		COMMENT ON COLUMN statistics_top_ten_shared_block_hits_queries.shared_buffer_read IS 'This is the read is the count of times disk blocks had to be read into memory, which indicates actual I/O operations. High values in shared_blks_read suggest that these queries are the most I/O intensive, which can be a starting point for performance optimization.';
		COMMENT ON COLUMN statistics_top_ten_shared_block_hits_queries.query IS 'This is the SQL source of the query.';

	END IF;

END $$;

/**
 * Top ten queries by block writes
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) AS pg_stat_statements_exists
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed
		view_source :=
$string$
CREATE OR REPLACE VIEW statistics_top_ten_block_writes_queries AS
SELECT s.userid::regrole AS rolename
	, d.datname AS database_name
	, s.shared_blks_written AS shared_buffer_written
	, s.shared_blks_read AS shared_buffer_read
	, s.query
FROM pg_stat_statements AS s
INNER JOIN pg_catalog.pg_database AS d
	ON s.dbid = d.oid
ORDER BY s.shared_blks_written DESC
LIMIT 10
;
$string$
;
		EXECUTE view_source;

		COMMENT ON VIEW statistics_top_ten_block_writes_queries IS 'Top ten queries by block writes.';
		COMMENT ON COLUMN statistics_top_ten_block_writes_queries.rolename IS 'The role/user who executed the query.';
		COMMENT ON COLUMN statistics_top_ten_block_writes_queries.database_name IS 'The database name in which the query has been executed.';
		COMMENT ON COLUMN statistics_top_ten_block_writes_queries.shared_buffer_written IS 'This is the number of shared blocks written into shared buffer and written to disk.';
		COMMENT ON COLUMN statistics_top_ten_block_writes_queries.shared_buffer_read IS 'This is the read is the count of times disk blocks had to be read into memory, which indicates actual I/O operations. High values in shared_blks_read suggest that these queries are the most I/O intensive, which can be a starting point for performance optimization.';
		COMMENT ON COLUMN statistics_top_ten_block_writes_queries.query IS 'This is the SQL source of the query.';

	END IF;

END $$;

/**
 * Top ten queries by WAL records generated, works on primary only
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
	view_source TEXT;
BEGIN

	SELECT count(*) AS pg_stat_statements_exists
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed
		view_source :=
$string$
CREATE OR REPLACE VIEW statistics_top_ten_wal_records_generated_queries AS
SELECT s.userid::regrole AS rolename
	, d.datname AS database_name
	, s.wal_records
	, s.query
FROM pg_stat_statements AS s
INNER JOIN pg_catalog.pg_database AS d
	ON s.dbid = d.oid
ORDER BY s.wal_records DESC
LIMIT 10
;
$string$
;
		EXECUTE view_source;

		COMMENT ON VIEW statistics_top_ten_wal_records_generated_queries IS 'Top ten queries by WAL records generated, works on primary only.';
		COMMENT ON COLUMN statistics_top_ten_wal_records_generated_queries.rolename IS 'The role/user who executed the query.';
		COMMENT ON COLUMN statistics_top_ten_wal_records_generated_queries.database_name IS 'The database name in which the query has been executed.';
		COMMENT ON COLUMN statistics_top_ten_wal_records_generated_queries.wal_records IS 'This is the total number of WAL records generated by the query.';
		COMMENT ON COLUMN statistics_top_ten_wal_records_generated_queries.query IS 'This is the SQL source of the query.';

	END IF;

END $$;

/**
 * Query statistics about how often the query has been called and the roles and
  * applications
 * NOTE: The schema where the extension pg_stat_statements is installed
 *       has to be in the search_path of the user!
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

DO $$
DECLARE
	pg_extension_installed BOOLEAN;
 	version_greater_13 BOOLEAN;
BEGIN

	SELECT count(*) > 0 AS pg_stat_statements_exists
	FROM pg_extension
	WHERE extname = 'pg_stat_statements'
	INTO pg_extension_installed
	;

	IF pg_extension_installed THEN
		-- The view is only created when pgstattuple is installed

	 	SELECT to_number((string_to_array(version(), ' '))[2], '999.99') >= 14
		INTO version_greater_13;
	
	 	IF version_greater_13 THEN
	 		-- Create the view for PostgreSQL 14 or newer

			CREATE OR REPLACE VIEW statistics_query_activity AS
			SELECT psa.datname AS database_name
				, psa.usename AS  rolename
				, psa.backend_type
				, psa.application_name
				, psa.query
				, pss.calls AS calls_by_all_rolls
			FROM pg_stat_activity AS psa
			LEFT OUTER JOIN pg_stat_statements AS pss
				ON psa.query_id = pss.queryid
			;

			COMMENT ON VIEW statistics_query_activity IS 'Query statistics about how often the query has been called and the roles and applications.';
			COMMENT ON COLUMN statistics_query_activity.database_name IS 'The name of the database.';
			COMMENT ON COLUMN statistics_query_activity.rolename IS 'The role/user who executed the query.';
			COMMENT ON COLUMN statistics_query_activity.backend_type IS 'The type of current backend. Possible types are autovacuum launcher, autovacuum worker, logical replication launcher, logical replication worker, parallel worker, background writer, client backend, checkpointer, archiver, standalone backend, startup, walreceiver, walsender, walwriter and walsummarizer. In addition, background workers registered by extensions may have additional types.';
			COMMENT ON COLUMN statistics_query_activity.application_name IS 'The name of the application that owns the backend, maybe empty when not set by the application.';
			COMMENT ON COLUMN statistics_query_activity.query IS 'This is the SQL source of the query.';
			COMMENT ON COLUMN statistics_query_activity.calls_by_all_rolls IS 'The overall count of executions of this query, the same query can be executed by different roles.';
		END IF;
	END IF;

END $$;

/**
 * View to monitor write ahead logs (WAL).
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW monitoring_wal AS
SELECT count (*) FILTER
		(
			WHERE pg_ls_dir~'done'
		) AS wals_done
	, count(*) FILTER
		(
			WHERE pg_ls_dir~'ready'
		) AS wals_ready
from pg_ls_dir('pg_wal/archive_status')
;

COMMENT ON VIEW monitoring_wal IS 'View to monitor write ahead logs (WAL).';
COMMENT ON COLUMN monitoring_wal.wals_done IS 'Number of WAL files, that have been done.';
COMMENT ON COLUMN monitoring_wal.wals_ready IS 'Number of WAL files, that have to be done.';

/**
 * View to monitor write ahead log (WAL) archiving
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW monitoring_wal_archiving AS
SELECT psa.archived_count
	, psa.failed_count
	, psa.last_archived_time
	, psa.last_archived_wal
	, psa.last_failed_time
	, psa.last_failed_wal
	, psa.stats_reset
FROM pg_catalog.pg_stat_archiver AS psa
;

COMMENT ON VIEW monitoring_wal_archiving IS 'View to monitor write ahead log (WAL) archiving.';
COMMENT ON COLUMN monitoring_wal_archiving.archived_count IS 'The number of WAL files that have been successfully archived.';
COMMENT ON COLUMN monitoring_wal_archiving.failed_count IS 'The number of failed attempts for archiving WAL files.';
COMMENT ON COLUMN monitoring_wal_archiving.last_archived_time IS 'Last time WAL files have been archived.';
COMMENT ON COLUMN monitoring_wal_archiving.last_archived_wal IS 'The name of the WAL file most recently successfully archived.';
COMMENT ON COLUMN monitoring_wal_archiving.last_failed_time IS 'The last time WAL files failed to be archived.';
COMMENT ON COLUMN monitoring_wal_archiving.last_failed_wal IS 'The name of the WAL file that most recently failed to get archived.';
COMMENT ON COLUMN monitoring_wal_archiving.stats_reset IS 'The last time the WAL statistics have been reset.';

/**
 * Monitoring active database connections and their lock state
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW monitoring_active_locks AS
SELECT DISTINCT l.pid
	, a.state
	, a.datname AS database_name
	, a.usename AS rolename
	, a.application_name
	, a.client_addr AS client_address
	, a.query_start
	, age (now(), a.query_start) AS query_age
	, a.wait_event_type
	, a.wait_event
	, l.locktype
	, l.mode
	, a.query
FROM pg_catalog.pg_stat_activity AS a
INNER JOIN pg_catalog.pg_locks AS l
	USING(pid)
;

COMMENT ON VIEW monitoring_active_locks IS 'Monitoring active database connections and their lock state';
COMMENT ON COLUMN monitoring_active_locks.pid IS 'The process id of the backend, needed in case a connection should be terminated with the function pg-terminate_backend(pid). For details see [SERVER SIGNALING FUNCTIONS](https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SIGNAL).';
COMMENT ON COLUMN monitoring_active_locks.state IS 'The current state of the connection.';
COMMENT ON COLUMN monitoring_active_locks.database_name IS 'The database name in which the queries have been executed.';
COMMENT ON COLUMN monitoring_active_locks.rolename IS 'The role/user who executed the queries.';
COMMENT ON COLUMN monitoring_active_locks.application_name IS 'The name of the application, could be empty if not set by a client.';
COMMENT ON COLUMN monitoring_active_locks.client_address IS 'The clients IP address.';
COMMENT ON COLUMN monitoring_active_locks.query_start IS 'The timestamp of the start of the query.';
COMMENT ON COLUMN monitoring_active_locks.query_age IS 'How long the query is already running.';
COMMENT ON COLUMN monitoring_active_locks.wait_event_type IS 'The type of event for which the backend is waiting, if any; otherwise NULL, for details see [WAIT EVENT TABLE](https://www.postgresql.org/docs/current/monitoring-monitoring_html#WAIT-EVENT-TABLE).';
COMMENT ON COLUMN monitoring_active_locks.wait_event IS 'Wait event name if backend is currently waiting, otherwise NULL. For details see [WAIT EVENT ACTIVITY TABLE](https://www.postgresql.org/docs/current/monitoring-monitoring_html#WAIT-EVENT-ACTIVITY-TABLE) and [WAIT EVENT TIMEOUT TABLE](https://www.postgresql.org/docs/current/monitoring-monitoring_html#WAIT-EVENT-TIMEOUT-TABLE)';
COMMENT ON COLUMN monitoring_active_locks.locktype IS 'The type of the lockable object: relation, extend, frozenid, page, tuple, transactionid, virtualxid, spectoken, object, userlock, advisory, or applytransaction, see [Wait Events of Type Lock](https://www.postgresql.org/docs/current/monitoring-stats.html#WAIT-EVENT-LOCK-TABLE).';
COMMENT ON COLUMN monitoring_active_locks.mode IS 'The name of the lock mode held or desired by this process. For details see [TABLE LEVEL LOCKS](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-TABLES) and [SERIALIZABLE ISOLATION LEVEL](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-SERIALIZABLE).';COMMENT ON COLUMN monitoring_active_locks.query IS 'This is the SQL source of the query.';

/**
 * Replication monitoring of primaries and followers.
 * Columns with names starting with standby_ are important to monitor followers.
 * Monitoring itself is done on the current primary.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW monitoring_replication AS
SELECT r.pid
	, r.usesysid AS oid_user
	, r.usename AS user_name
	, r.application_name
	, r.client_addr
	, r.client_hostname
	, r.client_port
	, r.backend_start
	, r.backend_xmin
	, r.state
	, r.sent_lsn AS last_wal_sent
	, r.write_lsn AS standby_lastwal_written
	, r.replay_lsn AS standby_last_wal_replayed
	, r.flush_lsn AS standby_last_wal_flushed
	, r.write_lag AS standby_wal_write_lag
	, r.flush_lag AS standby_flush_lag
	, r.replay_lag  AS standby_wal_replay_lag
	, r.sync_priority AS standby_sync_priority
	, r.sync_state AS standby_sync_state
	, r.reply_time AS last_replay_message_from_standby
FROM pg_catalog.pg_stat_replication AS r
;

COMMENT ON VIEW monitoring_replication IS 'Replication monitoring of primaries and followers. Columns with names starting with standby_ are important to monitor followers. Monitoring itself is done on the current primary.';
COMMENT ON COLUMN monitoring_replication.pid IS 'The process id of the WAL sender process.';
COMMENT ON COLUMN monitoring_replication.oid_user IS 'The OID of the user logged into this WAL sender process.';
COMMENT ON COLUMN monitoring_replication.user_name IS 'The name of user the logged into this WAL sender process.';
COMMENT ON COLUMN monitoring_replication.application_name IS 'The name of the application that is connected to this WAL sender.';
COMMENT ON COLUMN monitoring_replication.client_addr IS 'The IP address of the client connected to this WAL sender. If this field is null, it indicates that the client is connected via a Unix socket on the server machine.';
COMMENT ON COLUMN monitoring_replication.client_hostname IS 'The host name of the connected client, as reported by a reverse DNS lookup of client_addr. This field will only be non-null for IP connections, and only when log_hostname is enabled.';
COMMENT ON COLUMN monitoring_replication.client_port IS 'The TCP port number that the client is using for communication with this WAL sender, or -1 if a Unix socket is used.';
COMMENT ON COLUMN monitoring_replication.backend_start IS 'The time when this process was started, i.e., when the client connected to this WAL sender.';
COMMENT ON COLUMN monitoring_replication.backend_xmin IS 'This standby''s xmin horizon reported by [hot_standby_feedback](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-HOT-STANDBY-FEEDBACK).';
COMMENT ON COLUMN monitoring_replication.state IS 'The current WAL sender state. Possible values are: startup: This WAL sender is starting up.  catchup: This WAL sender''s connected standby is catching up with the primary. streaming: This WAL sender is streaming changes after its connected standby server has caught up with the primary. backup: This WAL sender is sending a backup. stopping: This WAL sender is stopping.';
COMMENT ON COLUMN monitoring_replication.last_wal_sent IS 'The last write-ahead log location sent on this connection.';
COMMENT ON COLUMN monitoring_replication.standby_last_wal_replayed IS 'The last write-ahead log location replayed into the database on this standby server';
COMMENT ON COLUMN monitoring_replication.standby_wal_replay_lag IS 'The last write-ahead log location replayed into the database on this standby server.';
COMMENT ON COLUMN monitoring_replication.standby_sync_priority IS 'The priority of this standby server for being chosen as the synchronous standby in a priority-based synchronous replication. This has no effect in a quorum-based synchronous replication.';
COMMENT ON COLUMN monitoring_replication.standby_sync_state IS 'Synchronous state of this standby server. Possible values are: async: This standby server is asynchronous. potential: This standby server is now asynchronous, but can potentially become synchronous if one of current synchronous ones fails. sync: This standby server is synchronous. quorum: This standby server is considered as a candidate for quorum standbys.';
COMMENT ON COLUMN monitoring_replication.last_replay_message_from_standby IS 'The send time of last reply message received from this standby server';

/**
 * This view returns numbers of conflicts happend on standby servers, AKA followers.
 * Therefore the qeury has to run against the followers.
 * The conflicts are shown per database.
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

 DO $$
 DECLARE
  version_greater_15 BOOLEAN;
 BEGIN
  SELECT to_number((string_to_array(version(), ' '))[2], '999.99') >= 16 INTO version_greater_15;

  IF version_greater_15 THEN
		CREATE OR REPLACE VIEW monitoring_database_conflicts AS
		SELECT c.datid AS database_id
			, c.datname AS database_name
			, c.confl_tablespace AS conflicts_tablespace
			, c.confl_lock AS conflicts_locks
			, c.confl_snapshot AS conflichts_snapshot
			, c.confl_bufferpin AS conflichts_pinned_buffers
			, c.confl_deadlock AS conflicts_deadlocks
			, c.confl_active_logicalslot AS conflicts_logical_replication_slots
		FROM pg_catalog.pg_stat_database_conflicts AS c
		;

		COMMENT ON VIEW monitoring_database_conflicts IS 'This view returns numbers of conflicts happend on standby servers, AKA followers. Therefore the qeury has to run against the followers. The conflicts are shown per database.';
		COMMENT ON COLUMN monitoring_database_conflicts.database_id IS 'The OID of this database.';
		COMMENT ON COLUMN monitoring_database_conflicts.database_name IS 'The name of this database.';
		COMMENT ON COLUMN monitoring_database_conflicts.conflicts_tablespace IS 'The number of queries in this database that have been canceled due to dropped tablespaces.';
		COMMENT ON COLUMN monitoring_database_conflicts.conflicts_locks IS 'The number of queries in this database that have been canceled due to lock timeouts.';
		COMMENT ON COLUMN monitoring_database_conflicts.conflichts_snapshot IS 'The number of queries in this database that have been canceled due to old snapshots.';
		COMMENT ON COLUMN monitoring_database_conflicts.conflichts_pinned_buffers IS 'The number of queries in this database that have been canceled due to pinned buffers.';
		COMMENT ON COLUMN monitoring_database_conflicts.conflicts_deadlocks IS 'The number of queries in this database that have been canceled due to deadlocks.';
		COMMENT ON COLUMN monitoring_database_conflicts.conflicts_logical_replication_slots IS 'The number of uses of logical slots in this database that have been canceled due to old snapshots or too low a wal_level on the primary server.';
	ELSE
	CREATE OR REPLACE VIEW monitoring_database_conflicts AS
	SELECT c.datid AS database_id
		, c.datname AS database_name
		, c.confl_tablespace AS conflicts_tablespace
		, c.confl_lock AS conflicts_locks
		, c.confl_snapshot AS conflichts_snapshot
		, c.confl_bufferpin AS conflichts_pinned_buffers
		, c.confl_deadlock AS conflicts_deadlocks
	FROM pg_catalog.pg_stat_database_conflicts AS c
	;

	COMMENT ON VIEW monitoring_database_conflicts IS 'This view returns numbers of conflicts happend on standby servers, AKA followers. Therefore the qeury has to run against the followers. The conflicts are shown per database.';
	COMMENT ON COLUMN monitoring_database_conflicts.database_id IS 'The OID of this database.';
	COMMENT ON COLUMN monitoring_database_conflicts.database_name IS 'The name of this database.';
	COMMENT ON COLUMN monitoring_database_conflicts.conflicts_tablespace IS 'The number of queries in this database that have been canceled due to dropped tablespaces.';
	COMMENT ON COLUMN monitoring_database_conflicts.conflicts_locks IS 'The number of queries in this database that have been canceled due to lock timeouts.';
	COMMENT ON COLUMN monitoring_database_conflicts.conflichts_snapshot IS 'The number of queries in this database that have been canceled due to old snapshots.';
	COMMENT ON COLUMN monitoring_database_conflicts.conflichts_pinned_buffers IS 'The number of queries in this database that have been canceled due to pinned buffers.';
	COMMENT ON COLUMN monitoring_database_conflicts.conflicts_deadlocks IS 'The number of queries in this database that have been canceled due to deadlocks.';
	END IF;
END $$;

/**
 * Retuns the current status about blocked user and query information
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW monitoring_blocked_and_blocking_activity AS
SELECT blocked_locks.pid AS blocked_pid
	, blocking_locks.database AS database_name
	, blocked_activity.usename AS blocked_rolename
	, blocking_locks.pid AS blocking_pid
	, blocking_activity.usename AS blocking_rolename
	, blocked_activity.query AS blocked_statement
	, blocking_activity.query AS current_statement_in_blocking_procblocked_statementess
	, age(now(), blocked_activity.query_start) AS blocked_query_age
	, age(now(), blocking_activity.query_start) AS blocking_query_age
FROM pg_catalog.pg_locks AS blocked_locks
INNER JOIN pg_catalog.pg_stat_activity AS blocked_activity
	ON blocked_activity.pid = blocked_locks.pid
INNER JOIN pg_catalog.pg_locks AS blocking_locks
	ON blocking_locks.locktype = blocked_locks.locktype
	AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
	AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
	AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
	AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
	AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
	AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
	AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
	AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
	AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
	AND blocking_locks.pid != blocked_locks.pid
INNER JOIN pg_catalog.pg_stat_activity AS blocking_activity
	ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.GRANTED
;

COMMENT ON VIEW monitoring_blocked_and_blocking_activity IS 'Retuns the current status about blocked user and query information.';
COMMENT ON COLUMN monitoring_blocked_and_blocking_activity.blocked_pid IS 'The connection identifier, that is blocked.';
COMMENT ON COLUMN monitoring_blocked_and_blocking_activity.database_name IS 'The database name in which the query has been executed.';
COMMENT ON COLUMN monitoring_blocked_and_blocking_activity.blocked_rolename IS 'The role/user who is blocked.';
COMMENT ON COLUMN monitoring_blocked_and_blocking_activity.blocking_pid IS 'The connection identifier, that is blocking.';
COMMENT ON COLUMN monitoring_blocked_and_blocking_activity.blocking_rolename IS 'The role/user who is blocking.';
COMMENT ON COLUMN monitoring_blocked_and_blocking_activity.blocked_statement IS 'This is the SQL source of the query that is blocked.';
COMMENT ON COLUMN monitoring_blocked_and_blocking_activity.current_statement_in_blocking_procblocked_statementess IS 'This is the SQL source of the query that is currently blocking.';
COMMENT ON COLUMN monitoring_blocked_and_blocking_activity.blocked_query_age IS 'That is the duration, that the query is blocked.';
COMMENT ON COLUMN monitoring_blocked_and_blocking_activity.blocking_query_age IS 'That is the duration, that the query is blocking.';

/**
 * View to monitor a follower WAL status
 *
 * NOTE: This view is only working on a follower node, on a primary it will
 * throw an error
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW monitoring_follower_wal_status AS
SELECT statement_timestamp () AS timestamp_execution
	, pg_read_file('/etc/hostname') AS hostname
	, pg_is_in_recovery () AS in_recovery
	, pg_is_wal_replay_paused () AS wal_replay_paused
	, pg_last_wal_receive_lsn () AS last_wal_receive_lsn
	, pg_last_wal_replay_lsn () AS last_wal_replay_lsn
	, pg_last_xact_replay_timestamp () AS last_xact_replay_timestamp
;

COMMENT ON VIEW monitoring_follower_wal_status IS 'View to monitor a follower WAL status.';
COMMENT ON COLUMN monitoring_follower_wal_status.timestamp_execution IS 'Timestamp at exection of the statement.';
COMMENT ON COLUMN monitoring_follower_wal_status.hostname IS 'Does return the hostname on Linux servers.';
COMMENT ON COLUMN monitoring_follower_wal_status.in_recovery IS 'Whether the follower is in recovery state, or not.';
COMMENT ON COLUMN monitoring_follower_wal_status.wal_replay_paused IS 'Whether the WAL replay on the server has been paused.';
COMMENT ON COLUMN monitoring_follower_wal_status.last_wal_receive_lsn IS 'The last WAL segment, that has been received, type is [pg_lsn](https://www.postgresql.org/docs/current/datatype-pg-lsn.html).';
COMMENT ON COLUMN monitoring_follower_wal_status.last_wal_replay_lsn IS 'The last WAL segment, that has been replayed, type is [pg_lsn](https://www.postgresql.org/docs/current/datatype-pg-lsn.html).';
COMMENT ON COLUMN monitoring_follower_wal_status.last_xact_replay_timestamp IS 'Returns the last timestamp of a WAL segment replayed during recovery.';

/**
 * This view is monitoring active vacuum progress
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE VIEW monitoring_vacuum AS
SELECT spv.datname AS database_name
	, c.relnamespace::RegClass AS schema_name
	, spv.relid::RegClass AS table_name
	, spv.pid process_id
	,CASE phase
		WHEN 'scanning heap' THEN
				CASE
					WHEN spv.heap_blks_total > 0 THEN
						round(spv.heap_blks_scanned / spv.heap_blks_total, 1)
					ELSE
						0::float
					END
		WHEN 'vacuuming heap' THEN
			CASE WHEN spv.heap_blks_total > 0 THEN
				round(spv.heap_blks_vacuumed / spv.heap_blks_total, 1)
			ELSE
				0::float
			END
		ELSE
			NULL::float
	END AS progress
FROM pg_stat_progress_vacuum AS spv
INNER JOIN pg_class AS c
	ON spv.relid = c.oid
;

COMMENT ON VIEW monitoring_vacuum IS 'This view returns the current progress of active vacuum calls.';
COMMENT ON COLUMN monitoring_vacuum.database_name IS 'The name of the database.';
COMMENT ON COLUMN monitoring_vacuum.schema_name IS 'The name of the schema.';
COMMENT ON COLUMN monitoring_vacuum.table_name IS 'The name of the table.';
COMMENT ON COLUMN monitoring_vacuum.process_id IS 'The backend process id (pid).';
COMMENT ON COLUMN monitoring_vacuum.progress IS 'Current state of the vacuum process.';

