dnl pg_isok A query centered monitoring tool for PostgreSQL
dnl Copyright (C) 2015, 2025 The Meme Factory, Inc. http://www.karlpinc.com/
dnl
dnl This program is free software: you can redistribute it and/or modify
dnl it under the terms of the GNU Affero General Public License as published
dnl by the Free Software Foundation, either version 3 of the License, or
dnl (at your option) any later version.
dnl
dnl This program is distributed in the hope that it will be useful,
dnl but WITHOUT ANY WARRANTY; without even the implied warranty of
dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
dnl GNU Affero General Public License for more details.
dnl
dnl You should have received a copy of the GNU Affero General Public License
dnl along with this program. If not, see .
dnl
dnl Run_isok_queries function.
dnl
dnl Karl O. Pinc
dnl
dnl
dnl Remarks:
dnl If the m4 macro `isok_disable_role' is defined, the code related
dnl to SET ROLE is omitted.
dnl
dnl m4 includes
include(`copyright.m4')
include(`constants.m4')
dnl
dnl plpgsql fragment to define columns returned
dnl
dnl Syntax: _return_cols()
dnl
dnl
changequote({,})
define({_return_cols},{
irid @extschema@.isok_results.irid%TYPE
, iqname @extschema@.isok_results.iqname%TYPE
, first_seen @extschema@.isok_results.first_seen%TYPE
, last_seen @extschema@.isok_results.last_seen%TYPE
, last_role @extschema@.isok_results.last_role%TYPE
, last_schemas @extschema@.isok_results.last_schemas%TYPE
, deferred_to @extschema@.isok_results.deferred_to%TYPE
, category @extschema@.isok_results.category%TYPE
, keep_until @extschema@.isok_results.keep_until%TYPE
, qr_id @extschema@.isok_results.qr_id%TYPE
, qr_message @extschema@.isok_results.qr_message%TYPE
, qr_extra @extschema@.isok_results.qr_extra%TYPE
, notes @extschema@.isok_results.notes%TYPE
})dnl
changequote(`,')dnl
dnl plpgsql fragment to put exception diagnostic info into vars.
dnl
dnl Syntax: _get_exception_info(msg, detail, hint, context)
dnl
dnl Input:
dnl names of variables in which to save exception info
dnl
changequote({,})
define({_get_exception_info},{
GET STACKED DIAGNOSTICS
$1 = MESSAGE_TEXT
, $2 = PG_EXCEPTION_DETAIL
, $3 = PG_EXCEPTION_HINT
, $4 = PG_EXCEPTION_CONTEXT;
$1 := E'\n' || $1;
IF $2 <> '' THEN
$1 := $1 || E'\n';
$2 := E'\nThe detail is:\n' || $2;
END IF;
IF $3 <> '' THEN
$2 := $2 || E'\n';
$3 := E'\nThe hint is:\n' || $3;
END IF;
$3 := $3 || E'\n';
IF $4 <> '' THEN
$4 := E'\n\nAdditional diagnostic information:\n' || $4;
END IF;
})dnl
changequote(`,')dnl
dnl plpgsql fragment for the list of exceptions trapped when executing
dnl user-supplied sql.
dnl
dnl Syntax: _user_sql_exceptions()
dnl
dnl We want all of these to trap most that could go wrong when a
dnl user-supplied bit of sql is executed:
dnl feature_not_supported (0A000)
dnl data_exception (22000) bad expressions (divide by 0, etc.)
dnl integrity_constraint_violation (23000) violating insert/update
dnl invalid_sql_statement_name (26000)
dnl sql_routine_exception (2F000)
dnl syntax_error_or_access_rule_violation (42000)
dnl
changequote({,})
define({_user_sql_exceptions},{feature_not_supported
OR sql_statement_not_yet_complete
OR data_exception
OR integrity_constraint_violation
OR invalid_sql_statement_name
OR sql_routine_exception
OR syntax_error_or_access_rule_violation})dnl
changequote(`,')dnl
CREATE OR REPLACE FUNCTION run_isok_queries()
RETURNS TABLE(_return_cols())
LANGUAGE plpgsql
SECURITY INVOKER
AS $$
-- Run all queries in ISOK_QUERIES.
--
-- AGPL_notice(` --', `2015, 2025',
`The Meme Factory, Inc. http://www.karlpinc.com/')
--
-- Syntax: run_isok_queries()
--
-- Arguments:
--
-- Remarks:
--
-- Bugs:
DECLARE
BEGIN
RETURN QUERY EXECUTE 'SELECT * FROM @extschema@.run_isok_queries(
$RIQ$SELECT @extschema@.isok_queries.iqname
FROM @extschema@.isok_queries
ORDER BY @extschema@.isok_queries.iqname$RIQ$
)';
END;
$$;
dnl Change the m4 quotes so we can use the ifdef macro with quotes
changequote({,})dnl
CREATE OR REPLACE FUNCTION run_isok_queries(query TEXT)
RETURNS TABLE(_return_cols())
LANGUAGE plpgsql
SECURITY INVOKER
AS $$
-- Run the queries in ISOK_QUERIES that have an IQName returned by
-- the SQL statement given as an argument.
--
-- AGPL_notice({ --}, {2015, 2025},
{The Meme Factory, Inc. http://www.karlpinc.com/})
--
-- Syntax: run_isok_queries(query)
--
-- Arguments:
-- query A query returning 1 column, of INTEGRITY_QUERY.IQName values.
--
-- Remarks:
--
-- Bugs:
DECLARE
-- From query results
this_iqname @extschema@.isok_queries.iqname%TYPE;
this_keep @extschema@.isok_queries.keep%TYPE;
ifdef({isok_disable_role},{},{dnl
saved_role @extschema@.isok_queries.role%TYPE;
}){}dnl
ifdef({isok_disable_search_path},{},{dnl
saved_search_path @extschema@.isok_queries.search_path%TYPE;
}){}dnl
saved_query @extschema@.isok_queries.query%TYPE;
-- Dynamic commands
ifdef({isok_disable_search_path},{},{dnl
starting_search_path TEXT;
}){}dnl
ifdef({isok_disable_role},{},{dnl
role_used name;
become_starting_role TEXT;
become_saved_role TEXT;
}){}dnl
-- Saved query results
id TEXT;
msg TEXT;
extra JSONB;
-- Exception handling
my_message_text TEXT;
my_pg_exception_detail TEXT;
my_pg_exception_hint TEXT;
my_pg_exception_context TEXT;
exception_caught BOOLEAN := FALSE;
BEGIN
-- Run the supplied query and iterate over the saved queries.
FOR this_iqname IN EXECUTE query LOOP
-- Keep track of which query results we receive from the query
CREATE TEMP TABLE _riq_qr_ids AS
SELECT @extschema@.isok_results.qr_id
FROM @extschema@.isok_results
WITH NO DATA;
-- Get the saved query
SELECT
@extschema@.isok_queries.query, @extschema@.isok_queries.keep
ifdef({isok_disable_search_path},{},{dnl
, @extschema@.isok_queries.search_path
}){}dnl
ifdef({isok_disable_role},{},{dnl
, @extschema@.isok_queries.role
}){}dnl
INTO
saved_query , this_keep
ifdef({isok_disable_search_path},{},{dnl
, saved_search_path
}){}dnl
ifdef({isok_disable_role},{},{dnl
, saved_role
}){}dnl
FROM @extschema@.isok_queries
WHERE @extschema@.isok_queries.iqname = this_iqname;
IF NOT FOUND THEN
RAISE EXCEPTION data_exception USING
MESSAGE = 'Error in run_isok_queries(TEXT)'
, DETAIL = 'Supplied query returned a value ('
|| textualize({this_iqname})
|| ') that is not an existing'
|| ' ISOK_QUERIES.IQName value';
RETURN;
END IF;
ifdef({isok_disable_search_path},{},{dnl
IF saved_search_path IS NOT NULL THEN
-- Use the given search path while executing the query
starting_search_path := current_setting('search_path');
BEGIN
EXECUTE 'SET LOCAL search_path TO ' || saved_search_path || ';';
EXCEPTION
WHEN syntax_error_or_access_rule_violation THEN
_get_exception_info(my_message_text,
my_pg_exception_detail,
my_pg_exception_hint,
my_pg_exception_context)
exception_caught := TRUE;
RAISE EXCEPTION data_exception USING
MESSAGE = 'run_isok_queries:'
|| ' Error setting search_path before executing an'
|| ' ISOK_QUERIES.Query'
, DETAIL = 'Problem in the search_path with IQName = ('
|| this_iqname || E'):\n'
|| 'The error is: ' || my_message_text
|| my_pg_exception_detail
|| my_pg_exception_hint
|| E'\nThe ISOK_QUERIES.Search_Path is:\n'
|| saved_search_path
|| my_pg_exception_context;
END;
END IF;
}){}dnl
ifdef({isok_disable_role},{},{dnl
IF saved_role IS NULL THEN
role_used := current_user;
ELSE
-- The query has an associated role
role_used := saved_role;
-- Set the role, if any
-- Save commands to switch back and forth between roles
become_starting_role := 'SET LOCAL ROLE TO ' || current_user || ';';
become_saved_role := 'SET LOCAL ROLE TO ' || saved_role || ';';
BEGIN
EXECUTE become_saved_role;
EXCEPTION
-- We want all of these to trap most that could go wrong:
-- data_exception (22000) no such role, etc.
-- syntax_error_or_access_rule_violation (42000)
WHEN data_exception
OR syntax_error_or_access_rule_violation THEN
_get_exception_info(my_message_text,
my_pg_exception_detail,
my_pg_exception_hint,
my_pg_exception_context)
exception_caught := TRUE;
RAISE EXCEPTION data_exception USING
MESSAGE = 'run_isok_queries:'
|| ' Error setting role before executing an'
|| ' ISOK_QUERIES.Query'
, DETAIL = 'Problem in the role with IQName = ('
|| this_iqname || E'):\n'
|| 'The error is: ' || my_message_text
|| my_pg_exception_detail
|| my_pg_exception_hint
|| E'\nThe ISOK_QUERIES.Role is:\n'
|| saved_role
|| my_pg_exception_context;
END;
END IF;
}){}dnl
BEGIN
FOR id, msg, extra IN EXECUTE saved_query LOOP
ifdef({isok_disable_role},{},{dnl
IF saved_role IS NOT NULL THEN
-- We don't know that the role used to execute the query
-- has permissions to the ISOK_RESULTS table.
EXECUTE become_starting_role;
END IF;
}){}dnl
-- Remember that we processed the id.
INSERT INTO _riq_qr_ids (qr_id)
VALUES (id);
BEGIN
-- Make a new isok_results row.
INSERT INTO @extschema@.isok_results
(iqname , first_seen , last_seen
, last_role
, last_schemas
, keep_until
, qr_id, qr_message, qr_extra
, notes)
VALUES (this_iqname, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
ifdef({isok_disable_role},{dnl
, current_user
},{{}dnl
, role_used
}){}dnl
, current_schemas(TRUE)
, CASE WHEN this_keep
THEN 'infinity'::TIMESTAMP
ELSE NULL
END
, id, msg, extra
, '')
ON CONFLICT
ON CONSTRAINT "isok_unique_result_constraint"
-- Update the existing isok_results row.
DO UPDATE
SET last_seen = CURRENT_TIMESTAMP
ifdef({isok_disable_role},{dnl
, last_role = current_user
},{{}dnl
, last_role = role_used
}){}dnl
, last_schemas = current_schemas(TRUE)
, qr_message = msg
, qr_extra = extra
WHERE @extschema@.isok_results.iqname = this_iqname
AND @extschema@.isok_results.qr_id = id;
EXCEPTION
-- We want a different error message here because the
-- context is useless, so don't include that.
-- When something goes wrong here it's usually the values
-- returned by the supplied query. Trap everything,
-- just in case.
WHEN _user_sql_exceptions THEN
_get_exception_info(my_message_text,
my_pg_exception_detail,
my_pg_exception_hint,
my_pg_exception_context)
exception_caught := TRUE;
RAISE EXCEPTION data_exception USING
MESSAGE = 'run_isok_queries:'
|| ' Error storing the results of an'
|| ' ISOK_QUERIES.Query'
, DETAIL = 'Problem in the saved query with IQName = ('
|| this_iqname || E'):\n'
|| 'The error is: ' || my_message_text
|| my_pg_exception_detail
|| my_pg_exception_hint
|| E'\nThe query is:\n'
|| saved_query;
END;
ifdef({isok_disable_role},{},{dnl
IF saved_role IS NOT NULL THEN
-- Put the desired effective role back in place for
-- continued query execution. There is no documentation
-- of how row-level security policies execution is
-- affected by changing the effective role while a query
-- executes. So make sure the effective role does not
-- change while the query executes.
EXECUTE become_saved_role;
END IF;
}){}dnl
END LOOP;
EXCEPTION
-- We want all of these to trap most that could go wrong when a
-- user-supplied bit of sql is executed
WHEN _user_sql_exceptions THEN
_get_exception_info(my_message_text,
my_pg_exception_detail,
my_pg_exception_hint,
my_pg_exception_context)
exception_caught := TRUE;
RAISE EXCEPTION data_exception USING
MESSAGE = 'run_isok_queries:'
|| ' Error executing stored ISOK_QUERIES.Query'
, DETAIL = 'Problem in the saved query with IQName = ('
|| this_iqname || E'):\n'
|| 'The error is: ' || my_message_text
|| my_pg_exception_detail
|| my_pg_exception_hint
|| E'\nThe query is:\n'
|| saved_query
|| my_pg_exception_context;
END;
ifdef({isok_disable_role},{},{dnl
-- Reset the role to its initial state
IF saved_role IS NOT NULL THEN
EXECUTE become_starting_role;
END IF;
}){}dnl
ifdef({isok_disable_search_path},{},{dnl
-- Reset the search path to its initial state
IF saved_search_path IS NOT NULL THEN
EXECUTE 'SET LOCAL search_path TO ' || starting_search_path || ';';
END IF;
}){}dnl
-- Note that we've processed the saved query.
UPDATE @extschema@.isok_queries
SET last_run = CURRENT_TIMESTAMP
, first_run = COALESCE(first_run, CURRENT_TIMESTAMP)
WHERE @extschema@.isok_queries.iqname = this_iqname;
-- Delete old query results that are not found this time.
CREATE UNIQUE INDEX _riq_qr_ids_qr_id
ON _riq_qr_ids (qr_id);
DELETE FROM @extschema@.isok_results
WHERE @extschema@.isok_results.iqname = this_iqname
AND (@extschema@.isok_results.keep_until IS NULL
OR @extschema@.isok_results.keep_until
<= CURRENT_TIMESTAMP::TIMESTAMP(0))
AND NOT EXISTS(
SELECT 1
FROM _riq_qr_ids
WHERE @extschema@.isok_results.qr_id
= _riq_qr_ids.qr_id);
-- Cleanup
DROP TABLE _riq_qr_ids CASCADE;
-- Return the rows found with their new values.
RETURN QUERY
SELECT @extschema@.isok_results.irid
, @extschema@.isok_results.iqname
, @extschema@.isok_results.first_seen
, @extschema@.isok_results.last_seen
, @extschema@.isok_results.last_role
, @extschema@.isok_results.last_schemas
, @extschema@.isok_results.deferred_to
, @extschema@.isok_results.category
, @extschema@.isok_results.keep_until
, @extschema@.isok_results.qr_id
, @extschema@.isok_results.qr_message
, @extschema@.isok_results.qr_extra
, @extschema@.isok_results.notes
FROM @extschema@.isok_results
WHERE @extschema@.isok_results.iqname = this_iqname
AND @extschema@.isok_results.resolved IS NULL
AND (@extschema@.isok_results.deferred_to IS NULL
OR @extschema@.isok_results.deferred_to
<= CURRENT_TIMESTAMP::TIMESTAMP(0));
END LOOP; -- Over iqname
EXCEPTION
-- We want all of these to trap most that could go wrong when a
-- user-supplied bit of sql is executed
WHEN _user_sql_exceptions THEN
IF exception_caught THEN
GET STACKED DIAGNOSTICS
my_message_text = MESSAGE_TEXT
, my_pg_exception_detail = PG_EXCEPTION_DETAIL;
RAISE EXCEPTION data_exception USING
MESSAGE = my_message_text
, DETAIL = my_pg_exception_detail;
ELSE
_get_exception_info(my_message_text,
my_pg_exception_detail,
my_pg_exception_hint,
my_pg_exception_context)
RAISE EXCEPTION data_exception USING
MESSAGE = 'run_isok_queries:'
|| ' Error executing supplied query argument'
, DETAIL = E'\nThe error is: ' || my_message_text
|| my_pg_exception_detail
|| my_pg_exception_hint
|| E'\nThe query is:\n'
|| query
|| my_pg_exception_context;
END IF;
-- WHEN OTHERS THEN
-- RAISE EXCEPTION data_exception USING
-- MESSAGE = 'SQLSTATE ' || SQLSTATE || ': SQLERRM ' || SQLERRM;
END;
$$;
changequote(`,')dnl