Raise error without rollback in plpgsql/postgresql - postgresql

I have two stored functions. delete_item which deletes one item, logging it's success or failure in an actionlog table, it returns a 1 on errors and 0 when running successfully.
Secondly I have another function remove_expired that finds what to delete and loops through it calling delete_item.
All this is intended to be called using a simple bash script (hard requirement from operations, so calling like this is not up for discussion), and it have to give an error code when things doesn't work for their reporting tools.
We want all the deletions possible to succeed (we don't expect errors, but humans are still humans, and errors does happen), so if we want to delete 10 items and 1 fails, we still want the other 9 to be deleted.
Secondly we would really like the logs to be in the table actionlog both in the success and error case. I.e., we want that log to be complete.
Since plpgsql functions doesn't allow manual transaction management that seems to not be an option (unless I missed a way to circumvent this?).
The only way I've found so far to achieve this is to wrap scripts around it outside plpgsql, but we would very much like this to be possible in pure plpgsql so we can just give operations a pssql -C ... command and then they shouldn't be concerned with anything else.
SQL to reproduce the problem:
DROP FUNCTION IF EXISTS remove_expired(timestamp with time zone);
DROP FUNCTION IF EXISTS delete_item(integer);
DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;
CREATE TABLE test (
id serial primary key not null,
t timestamp with time zone not null
);
CREATE TABLE evil (
test_id integer not null references test(id)
);
CREATE TABLE actionlog (
eventTime timestamp with time zone not null default now(),
message text not null
);
INSERT INTO test (actualTime, t)
VALUES ('2020-04-01T10:00:00+0200'),
('2020-04-01T10:15:00+0200'), -- Will not be deleable due to foreign key
('2020-04-01T10:30:00+0200')
;
INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;
CREATE OR REPLACE FUNCTION remove_expired(timestamp with time zone)
RETURNS void
AS
$$
DECLARE
test_id int;
failure_count int = 0;
BEGIN
FOR test_id IN
SELECT id FROM test WHERE t < $1
LOOP
failure_count := delete_item(test_id) + failure_count;
END LOOP;
IF failure_count > 0 THEN
-- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
RAISE 'There was one or more errors deleting. See the log for details';
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_item(integer)
RETURNS integer
AS
$$
BEGIN
DELETE FROM test WHERE id = $1;
INSERT INTO actionlog (message)
VALUES ('Deleted with ID: ' || $1);
RETURN 0;
EXCEPTION WHEN OTHERS THEN
INSERT INTO actionlog (message)
VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
RETURN 1;
END
$$ LANGUAGE plpgsql;
Thanks in advance for any useful input

You can have something close to what your expect in PostgreSQL 11 or PostgreSQL 12 but only with procedures because as already said functions will always roll everything back in case of errors.
With:
DROP PROCEDURE IF EXISTS remove_expired(timestamp with time zone);
DROP PROCEDURE IF EXISTS delete_item(integer);
DROP FUNCTION IF EXISTS f_removed_expired;
DROP SEQUENCE IF EXISTS failure_count_seq;
DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;
CREATE TABLE test (
id serial primary key not null,
t timestamp with time zone not null
);
CREATE TABLE evil (
test_id integer not null references test(id)
);
CREATE TABLE actionlog (
eventTime timestamp with time zone not null default now(),
message text not null
);
INSERT INTO test (t)
VALUES ('2020-04-01T10:00:00+0200'),
('2020-04-01T10:15:00+0200'), -- Will not be removed due to foreign key
('2020-04-01T10:30:00+0200')
;
select * from test where t < current_timestamp;
INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;
CREATE SEQUENCE failure_count_seq MINVALUE 0;
SELECT SETVAL('failure_count_seq', 0, true);
CREATE OR REPLACE PROCEDURE remove_expired(timestamp with time zone)
AS
$$
DECLARE
test_id int;
failure_count int = 0;
return_code int;
BEGIN
FOR test_id IN
SELECT id FROM test WHERE t < $1
LOOP
call delete_item(test_id);
COMMIT;
END LOOP;
SELECT currval('failure_count_seq') INTO failure_count;
IF failure_count > 0 THEN
-- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
RAISE 'There was one or more errors deleting. See the log for details';
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE delete_item(in integer)
AS
$$
DECLARE
forget_value int;
BEGIN
DELETE FROM test WHERE id = $1;
INSERT INTO actionlog (message)
VALUES ('Deleted with ID: ' || $1);
EXCEPTION WHEN OTHERS THEN
INSERT INTO actionlog (message)
VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
COMMIT;
SELECT NEXTVAL('failure_count_seq') INTO forget_value;
END
$$ LANGUAGE plpgsql;
--
I get:
select * from test;
id | t
----+------------------------
1 | 2020-04-01 10:00:00+02
2 | 2020-04-01 10:15:00+02
3 | 2020-04-01 10:30:00+02
(3 rows)
select current_timestamp;
current_timestamp
-------------------------------
2020-04-01 16:52:26.171975+02
(1 row)
call remove_expired(current_timestamp);
psql:test.sql:80: ERROR: There was one or more errors deleting. See the log for details
CONTEXT: PL/pgSQL function remove_expired(timestamp with time zone) line 17 at RAISE
select currval('failure_count_seq');
currval
---------
1
(1 row)
select * from test;
id | t
----+------------------------
2 | 2020-04-01 10:15:00+02
(1 row)
select * from actionlog;
eventtime | message
-------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------
2020-04-01 16:52:26.172173+02 | Deleted with ID: 1
2020-04-01 16:52:26.179794+02 | Error deleting ID: 2. The error was: update or delete on table "test" violates foreign key constraint "evil_test_id_fkey" on table "evil"
2020-04-01 16:52:26.196503+02 | Deleted with ID: 3
(3 rows)
I use a sequence to record the number of failures: you can use this sequence to test for failures and return the right return code.

Related

user creation dumping using trigger and fucntions

As PostgreSQL doesn't dump object creation date so I want to manually dump user creation date by using trigger and functions. I have created trigger and functions but it's not working.
CREATE TABLE user_audits (
usesysid INT GENERATED ALWAYS AS IDENTITY,
usename varchar NOT NULL,
created_on TIMESTAMP(6) NOT NULL
);
============================
CREATE OR REPLACE FUNCTION user_creation()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
IF NEW.usename <> OLD.usename THEN
INSERT INTO user_audits(usesysid,usename,created_on)
VALUES(usesysid,usename,now());
END IF;
RETURN NEW;
END;
$$
=================================
CREATE TRIGGER user_creation
BEFORE UPDATE
ON user
FOR EACH ROW
EXECUTE PROCEDURE user_creation();
This is important for audit purpose, for now I am using log file to check creation date but it will rotate after sometime.
Please suggest the better way to dump user creation date in table so that I can retrieve the information anytime.
Thanks
I created a similar excercise with the following tables:
The user_tbl table having only a identity column usersysid and the username
CREATE TABLE user_tbl (
usersysid INT GENERATED ALWAYS AS IDENTITY,
username varchar NOT NULL
);
The user_audits table, slightly modified version of yours: where i added an id identity field. I removed the identity from the usersysid field (since it'll be populated with the one coming from user_tbl)
CREATE TABLE user_audits (
id INT GENERATED ALWAYS AS IDENTITY,
usersysid INT,
username varchar NOT NULL,
created_on TIMESTAMP(6) NOT NULL
);
Now the function, I check if the OLD.username is null, this means that is an insert, if NEW.username <> OLD.username then is an update.
CREATE OR REPLACE FUNCTION user_creation()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
IF OLD.username is null OR NEW.username <> OLD.username THEN
INSERT INTO user_audits(usersysid,username,created_on)
VALUES(NEW.usersysid,NEW.username,now());
END IF;
RETURN NEW;
END;
$$
;
And finally the trigger, which is fired both on INSERT or UPDATE
CREATE TRIGGER user_creation
BEFORE INSERT OR UPDATE
ON user_tbl
FOR EACH ROW
EXECUTE PROCEDURE user_creation();
Now if I create two new rows and update one with the following
insert into user_tbl (username) values('Carlo');
insert into user_tbl (username) values('Gianni');
update user_tbl set username='Giorgio' where usersysid=1;
I end up with the user_tbl containing the 2 expected rows
defaultdb=> select * from user_tbl;
usersysid | username
-----------+----------
2 | Gianni
1 | Giorgio
(2 rows)
and the user_audits tables containing 3 rows (2 for the insert + 1 for the update)
defaultdb=> select * from user_audits;
id | usersysid | username | created_on
----+-----------+----------+----------------------------
1 | 1 | Carlo | 2021-06-04 13:57:44.810889
2 | 2 | Gianni | 2021-06-04 13:58:14.680878
3 | 1 | Giorgio | 2021-06-04 13:58:44.702364
(3 rows)

Postgres Count records inserted/ updated

I'm trying to keey track of a clients database with which we sync. I need to record records_added (INSERTs) and records_updated (UPDATEs) to our table.
I'm using an UPSERT to handle the sync, and a trigger to update a table keeping track of insert/updates.
The issue is counting records that have are updated. I have 40+ columns to check, do I have to put all these in my check logic? Is there a more elegant way?
Section of code in question:
select
case
when old.uuid = new.uuid
and (
old.another_field != new.another_field,
old.and_another_field != new.and_another_field,
-- many more columns here << This is particularly painful
) then 1
else 0
end into update_count;
Reproducible example:
-- create tables
CREATE TABLE IF NOT EXISTS example (uuid serial primary key, another_field int, and_another_field int);
CREATE TABLE IF NOT EXISTS tracker_table (
records_added integer DEFAULT 0,
records_updated integer DEFAULT 0,
created_at date unique
);
-- create function
CREATE OR REPLACE FUNCTION update_records_inserted () RETURNS TRIGGER AS $body$
DECLARE update_count INT;
DECLARE insert_count INT;
BEGIN
-- ---------------- START OF BLOCK IN QUESTION -----------------
select
case
when old.uuid = new.uuid
and (
old.another_field != new.another_field
-- many more columns here
) then 1
else 0
end into update_count;
-- ------------------ END OF BLOCK IN QUESTION ------------------
-- count INSERTs
select
case
when old.uuid is null
and new.uuid is not null then 1
else 0
end into insert_count;
-- --log the counts
-- raise notice 'update %', update_count;
-- raise notice 'insert %', insert_count;
-- insert or update count to tracker table
insert into
tracker_table(
created_at,
records_added,
records_updated
)
VALUES
(CURRENT_DATE, insert_count, update_count) ON CONFLICT (created_at) DO
UPDATE
SET
records_added = tracker_table.records_added + insert_count,
records_updated = tracker_table.records_updated + update_count;
RETURN NEW;
END;
$body$ LANGUAGE plpgsql;
-- Trigger
DROP TRIGGER IF EXISTS example_trigger ON example;
CREATE TRIGGER example_trigger
AFTER
INSERT
OR
UPDATE
ON example FOR EACH ROW EXECUTE PROCEDURE update_records_inserted ();
-- A query to insert, then update when number of uses > 1
insert into example(whatever) values (2, 3) ON CONFLICT(uuid) DO UPDATE SET another_field=excluded.another_field+1;

Race condition in partitioning with dynamic table creation

I'm trying to implement table partitioning with dynamic table creation using BEFORE INSERT trigger to create new tables and indexes when necesarry using following solution:
create table mylog (
mylog_id serial not null primary key,
ts timestamp(0) not null default now(),
data text not null
);
CREATE OR REPLACE FUNCTION mylog_insert() RETURNS trigger AS
$BODY$
DECLARE
_name text;
_from timestamp(0);
_to timestamp(0);
BEGIN
SELECT into _name 'mylog_'||replace(substring(date_trunc('day', new.ts)::text from 0 for 11), '-', '');
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name=_name) then
SELECT into _from date_trunc('day', new.ts)::timestamp(0);
SELECT into _to _from + INTERVAL '1 day';
EXECUTE 'CREATE TABLE '||_name||' () INHERITS (mylog)';
EXECUTE 'ALTER TABLE '||_name||' ADD CONSTRAINT ts_check CHECK (ts >= '||quote_literal(_from)||' AND ts < '||quote_literal(_to)||')';
EXECUTE 'CREATE INDEX '||_name||'_ts_idx on '||_name||'(ts)';
END IF;
EXECUTE 'INSERT INTO '||_name||' (ts, data) VALUES ($1, $2)' USING
new.ts, new.data;
RETURN null;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER mylog_insert
BEFORE INSERT
ON mylog
FOR EACH ROW
EXECUTE PROCEDURE mylog_insert();
Everything works as expected but each day when concurrent INSERT statements are being fired for the first time that day, one of them fails trying to "create table that already exists". I suspect that this is caused by the triggers being fired concurrently and both trying to create new table and only one can succeed.
I could be using CREATE TABLE IF NOT EXIST but I cannot detect the outcome so I cannot reliably create constraints and indexes.
What can I do to avoid such problem? Is there any way to signal the fact that the table has been already created to other concurrent triggers? Or maybe there is a way of knowing if CREATE TABLE IF NOT EXISTS created new table or not?
What I do is create a pgAgent job to run every day and create 3 months of tables ahead of time.
CREATE OR REPLACE FUNCTION avl_db.create_alltables()
RETURNS numeric AS
$BODY$
DECLARE
rec record;
BEGIN
FOR rec IN
SELECT date_trunc('day', i::timestamp without time zone) as table_day
FROM generate_series(now()::date,
now()::date + '3 MONTH'::interval,
'1 DAY'::interval) as i
LOOP
PERFORM avl_db.create_table (rec.table_day);
END LOOP;
PERFORM avl_db.avl_partition(now()::date,
now()::date + '3 MONTH'::interval);
RETURN 0;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION avl_db.create_alltables()
OWNER TO postgres;
create_table is very similar to your CREATE TABLE code
avl_partition update the BEFORE INSERT TRIGGER but I saw you do that part with dynamic query. Will have to check again that.
Also I see you are doing inherit, but you are missing a very important CONSTRAINT
CONSTRAINT route_sources_20170601_event_time_check CHECK (
event_time >= '2017-06-01 00:00:00'::timestamp without time zone
AND event_time < '2017-06-02 00:00:00'::timestamp without time zone
)
This improve the query a lot when doing a search for event_time because doesn't have to check every table.
See how doesn't check all tables for the month:
Eventually I wrapped CREATE TABLE in BEGIN...EXCEPTION block that catches duplicate_table exception - this did the trick, but creating the tables upfront in a cronjob is much better approach performance-wise.
CREATE OR REPLACE FUNCTION mylog_insert() RETURNS trigger AS
$BODY$
DECLARE
_name text;
_from timestamp(0);
_to timestamp(0);
BEGIN
SELECT into _name 'mylog_'||replace(substring(date_trunc('day', new.ts)::text from 0 for 11), '-', '');
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name=_name) then
SELECT into _from date_trunc('day', new.ts)::timestamp(0);
SELECT into _to _from + INTERVAL '1 day';
BEGIN
EXECUTE 'CREATE TABLE '||_name||' () INHERITS (mylog)';
EXECUTE 'ALTER TABLE '||_name||' ADD CONSTRAINT ts_check CHECK (ts >= '||quote_literal(_from)||' AND ts < '||quote_literal(_to)||')';
EXECUTE 'CREATE INDEX '||_name||'_ts_idx on '||_name||'(ts)';
EXCEPTION WHEN duplicate_table THEN
RAISE NOTICE 'table exists -- ignoring';
END;
END IF;
EXECUTE 'INSERT INTO '||_name||' (ts, data) VALUES ($1, $2)' USING
new.ts, new.data;
RETURN null;
END;
$BODY$
LANGUAGE plpgsql;

Postgresql dynamic function with current table name

I have a function (audit.create_audit_table()) that accepts an array of table names. It creates a single function audit.if_modified_func() and then loops through each table name and creates an audit table and applies a trigger to the main table. The function compiles and is created with no errors. When I run the function
select audit.create_audit_table(ARRAY['organization'])
I keep getting the following error and I am not sure why because I thought that TG_TABLE_NAME is a automatic variable which will give me access to the current table that is executing audit.if_modified_func()
ERROR:
ERROR: column "tg_table_name" does not exist
LINE 3: audit_row audit.' || quote_ident(TG_TABLE_NAME::TEXT)||';
^
Here's the function:
CREATE OR REPLACE FUNCTION audit.create_audit_table(table_names character varying[])
RETURNS character varying AS
$BODY$
DECLARE
table_name varchar;
i int;
BEGIN
EXECUTE 'CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS TRIGGER AS $$
DECLARE
audit_row audit.' || quote_ident(TG_TABLE_NAME::TEXT)||';
include_values boolean;
log_diffs boolean;
h_old hstore;
h_new hstore;
excluded_cols text[] = ARRAY[]::text[];
BEGIN
IF TG_WHEN <> ''AFTER'' THEN
RAISE EXCEPTION ''audit.if_modified_func() may only run as an AFTER trigger'';
END IF;
audit_row = ROW(
nextval(''audit.'|| quote_ident(TG_TABLE_NAME::text) ||'_event_id_seq''), -- event_id
TG_TABLE_SCHEMA::text, -- schema_name
TG_TABLE_NAME::text, -- table_name
TG_RELID, -- relation OID for much quicker searches
session_user::text, -- session_user_name
current_timestamp, -- action_tstamp_tx
statement_timestamp(), -- action_tstamp_stm
clock_timestamp(), -- action_tstamp_clk
txid_current(), -- transaction ID
current_setting(''application_name''), -- client application
inet_client_addr(), -- client_addr
inet_client_port(), -- client_port
current_query(), -- top-level query or queries (if multistatement) from client
substring(TG_OP,1,1), -- action
NULL, NULL, -- row_data, changed_fields
''f'' -- statement_only
);
IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM ''f''::boolean THEN
audit_row.client_query = NULL;
END IF;
IF TG_ARGV[1] IS NOT NULL THEN
excluded_cols = TG_ARGV[1]::text[];
END IF;
IF (TG_OP = ''UPDATE'' AND TG_LEVEL = ''ROW'') THEN
audit_row.row_data = hstore(OLD.*) - excluded_cols;
audit_row.changed_fields = (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
IF audit_row.changed_fields = hstore('''') THEN
-- All changed fields are ignored. Skip this update.
RETURN NULL;
END IF;
ELSIF (TG_OP = ''DELETE'' AND TG_LEVEL = ''ROW'') THEN
audit_row.row_data = hstore(OLD.*) - excluded_cols;
ELSIF (TG_OP = ''INSERT'' AND TG_LEVEL = ''ROW'') THEN
audit_row.row_data = hstore(NEW.*) - excluded_cols;
ELSIF (TG_LEVEL = ''STATEMENT'' AND TG_OP IN (''INSERT'',''UPDATE'',''DELETE'',''TRUNCATE'')) THEN
audit_row.statement_only = ''t'';
ELSE
RAISE EXCEPTION ''[audit.if_modified_func] - Trigger func added as trigger for unhandled case: %%, %%'',TG_OP, TG_LEVEL;
RETURN NULL;
END IF;
INSERT INTO audit.'|| quote_ident(TG_TABLE_NAME::TEXT) ||' VALUES (audit_row.*);
RETURN null;
END;
$$
LANGUAGE plpgsql;
ALTER FUNCTION audit.if_modified_func()
OWNER TO postgres;';
FOR i in 1..array_upper(table_names, 1) LOOP
EXECUTE format('
DROP TABLE IF EXISTS audit.%1$s;
CREATE TABLE audit.%1$s (
event_id bigserial primary key,
schema_name text not null,
table_name text not null,
relid oid not null,
session_user_name text,
action_tstamp_tx TIMESTAMP WITH TIME ZONE NOT NULL,
action_tstamp_stm TIMESTAMP WITH TIME ZONE NOT NULL,
action_tstamp_clk TIMESTAMP WITH TIME ZONE NOT NULL,
transaction_id bigint,
application_name text,
client_addr inet,
client_port integer,
client_query text,
action TEXT NOT NULL CHECK (action IN (''I'',''D'',''U'', ''T'')),
row_data hstore,
changed_fields hstore,
statement_only boolean not null
);
REVOKE ALL ON audit.%1$s FROM public;
COMMENT ON TABLE audit.%1$s IS ''History of auditable actions on audited tables, from audit.if_modified_func()'';
COMMENT ON COLUMN audit.%1$s.event_id IS ''Unique identifier for each auditable event'';
COMMENT ON COLUMN audit.%1$s.schema_name IS ''Database schema audited table for this event is in'';
COMMENT ON COLUMN audit.%1$s.table_name IS ''Non-schema-qualified table name of table event occured in'';
COMMENT ON COLUMN audit.%1$s.relid IS ''Table OID. Changes with drop/create. Get with ''''tablename''''::regclass'';
COMMENT ON COLUMN audit.%1$s.session_user_name IS ''Login / session user whose statement caused the audited event'';
COMMENT ON COLUMN audit.%1$s.action_tstamp_tx IS ''Transaction start timestamp for tx in which audited event occurred'';
COMMENT ON COLUMN audit.%1$s.action_tstamp_stm IS ''Statement start timestamp for tx in which audited event occurred'';
COMMENT ON COLUMN audit.%1$s.action_tstamp_clk IS ''Wall clock time at which audited event''''s trigger call occurred'';
COMMENT ON COLUMN audit.%1$s.transaction_id IS ''Identifier of transaction that made the change. May wrap, but unique paired with action_tstamp_tx.'';
COMMENT ON COLUMN audit.%1$s.client_addr IS ''IP address of client that issued query. Null for unix domain socket.'';
COMMENT ON COLUMN audit.%1$s.client_port IS ''Remote peer IP port address of client that issued query. Undefined for unix socket.'';
COMMENT ON COLUMN audit.%1$s.client_query IS ''Top-level query that caused this auditable event. May be more than one statement.'';
COMMENT ON COLUMN audit.%1$s.application_name IS ''Application name set when this audit event occurred. Can be changed in-session by client.'';
COMMENT ON COLUMN audit.%1$s.action IS ''Action type; I = insert, D = delete, U = update, T = truncate'';
COMMENT ON COLUMN audit.%1$s.row_data IS ''Record value. Null for statement-level trigger. For INSERT this is the new tuple. For DELETE and UPDATE it is the old tuple.'';
COMMENT ON COLUMN audit.%1$s.changed_fields IS ''New values of fields changed by UPDATE. Null except for row-level UPDATE events.'';
COMMENT ON COLUMN audit.%1$s.statement_only IS ''''''t'''' if audit event is from an FOR EACH STATEMENT trigger, ''''f'''' for FOR EACH ROW'';
CREATE INDEX %1$s_relid_idx ON audit.%1$s(relid);
CREATE INDEX %1$s_action_tstamp_tx_stm_idx ON audit.%1$s(action_tstamp_stm);
CREATE INDEX %1$s_action_idx ON audit.%1$s(action);
', table_names[i]);
EXECUTE format('
DROP TRIGGER IF EXISTS audit_trigger_row ON %1$s;
CREATE TRIGGER audit_trigger_row
AFTER INSERT OR UPDATE OR DELETE
ON public.%1$s
FOR EACH ROW
EXECUTE PROCEDURE audit.if_modified_func();', table_names[i]);
EXECUTE format('
DROP TRIGGER IF EXISTS audit_trigger_stm ON %1$s;
CREATE TRIGGER audit_trigger_stm
AFTER TRUNCATE
ON public.%1$s
FOR EACH STATEMENT
EXECUTE PROCEDURE audit.if_modified_func();', table_names[i]);
END LOOP;
RETURN 'SUCCESS';
END;
$BODY$
LANGUAGE plpgsql;
ALTER FUNCTION audit.create_audit_table(character varying[])
OWNER TO postgres;
UPDATE 03/31:
Ok, so I created the if_modified_func() function without the dynamic sql and I declared the audit_row as audit_row RECORD; I am not sure about the part of "needing a cast upon inserting the values". I am also not sure if this is the correct way to do the insert
EXECUTE format($string$INSERT INTO audit.%1$s VALUES (audit_row.*);$string$, TG_TABLE_NAME::text);
I am now getting this error when I run select audit.create_audit_table(ARRAY['organization'])
ERROR:
ERROR: record "audit_row" has no field "row_data"
CONTEXT: PL/pgSQL function audit.if_modified_func() line 42 at assignment
Here's the updated function:
CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS TRIGGER AS $$
DECLARE
audit_row RECORD;
include_values boolean;
log_diffs boolean;
h_old hstore;
h_new hstore;
excluded_cols text[] = ARRAY[]::text[];
BEGIN
IF TG_WHEN <> 'AFTER' THEN
RAISE EXCEPTION 'audit.if_modified_func() may only run as an AFTER trigger';
END IF;
audit_row = ROW(
nextval(format('audit.%1$s_event_id_seq',TG_TABLE_NAME::text)), -- event_id
TG_TABLE_SCHEMA::text, -- schema_name
TG_TABLE_NAME::text, -- table_name
TG_RELID, -- relation OID for much quicker searches
session_user::text, -- session_user_name
current_timestamp, -- action_tstamp_tx
statement_timestamp(), -- action_tstamp_stm
clock_timestamp(), -- action_tstamp_clk
txid_current(), -- transaction ID
current_setting('application_name'), -- client application
inet_client_addr(), -- client_addr
inet_client_port(), -- client_port
current_query(), -- top-level query or queries (if multistatement) from client
substring(TG_OP,1,1), -- action
NULL, NULL, -- row_data, changed_fields
'f' -- statement_only
);
IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM 'f'::boolean THEN
audit_row.client_query = NULL;
END IF;
IF TG_ARGV[1] IS NOT NULL THEN
excluded_cols = TG_ARGV[1]::text[];
END IF;
IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
audit_row.row_data = hstore(OLD.*) - excluded_cols;
audit_row.changed_fields = (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
IF audit_row.changed_fields = hstore('') THEN
-- All changed fields are ignored. Skip this update.
RETURN NULL;
END IF;
ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
audit_row.row_data = hstore(OLD.*) - excluded_cols;
ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
audit_row.row_data = hstore(NEW.*) - excluded_cols;
ELSIF (TG_LEVEL = 'STATEMENT' AND TG_OP IN ('INSERT','UPDATE','DELETE','TRUNCATE')) THEN
audit_row.statement_only = 't';
ELSE
RAISE EXCEPTION '[audit.if_modified_func] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
RETURN NULL;
END IF;
EXECUTE format('INSERT INTO audit.%1$s VALUES (audit_row.*)', TG_TABLE_NAME::text);
RETURN null;
END;
$$
LANGUAGE plpgsql;
ALTER FUNCTION audit.if_modified_func()
OWNER TO postgres;
TG_TABLE_NAME is a special, trigger variable, which is only available inside trigger functions. Your create_audit_table() is not a trigger function.
Also, you constantly redefining your real trigger function (if_modified_func()), which "invalidates" any earlier created triggers.
Create your trigger function without the dynamic SQL magic (dynamic SQL will only need to insert values to these audit tables). Then, you can add your audit logic to a table with:
CREATE TRIGGER audit_trigger_row
AFTER INSERT OR UPDATE OR DELETE
ON public.<your_table_name>
FOR EACH ROW
EXECUTE PROCEDURE <your_audit_trigger_function_name>();
You can put this (but only this -- maybe with drop if exists) inside a function, to allow attaching this audit logic more easily.
Notes:
Inside the trigger function, you cannot use a %ROWTYPE variable (because you don't know the exact table. you only have its name). The solution is simple: just use the RECORD type instead (you will need a cast upon inserting the values though).
Don't use single quotes for such long strings. Use the $your_keyword$<string_value>$your_keyword$ format instead. With possibly the format() function instead of just concatenating values. Your code will be much more readable.
Edit: to utilize your RECORD variable, you should either:
Initialize it with a structure. You can do this in your case with f.ex.
SELECT nextval('audit.'|| quote_ident(TG_TABLE_NAME) || '_event_id_seq') AS event_id,
TG_TABLE_SCHEMA AS schema_name,
TG_TABLE_NAME AS table_name,
TG_RELID AS relid,
session_user AS session_user_name,
current_timestamp AS action_tstamp_tx,
statement_timestamp() AS action_tstamp_stm,
clock_timestamp() AS action_tstamp_clk,
txid_current() AS transaction_id,
current_setting('application_name') AS application_name,
inet_client_addr() AS client_addr,
inet_client_port() AS client_port,
current_query() AS client_query,
substring(TG_OP, 1, 1) AS action,
NULL::hstore AS row_data,
NULL::hstore AS changed_fields,
FALSE AS statement_only
INTO audit_row;
Use the predefined names of the ROW() constructor. The first column's name if f1, the second's is f2, etc.
audit_row.f15 = hstore(OLD.*) - excluded_cols;
After choosing one of the above methods, you should insert the row like:
EXECUTE format('INSERT INTO audit.%1$s VALUES (($1::text::audit.%1$s).*)', quote_ident(TG_TABLE_NAME)) USING audit_row;
Note: even the cast to text is required due to the fact that EXECUTE cannot know the actual structure of audit_row.
http://rextester.com/GUAJ1339
quote_ident(TG_TABLE_NAME::TEXT) will apply necessary actions to correcty quote the argument as relation name.
I would recommend using execute format('statement') instead of concatinations, eg:
t=# do $$ begin raise info '%',format('I am %I, now is %L',current_user,now()); end;$$;
INFO: I am postgres, now is '2017-03-30 07:33:53.579476+00'
DO
Instead of:
t=# do $$ begin raise info '%','I am '||quote_ident(current_user)||', now is '||quote_ident(now()::text); end;$$;
INFO: I am postgres, now is "2017-03-30 07:36:20.495887+00"
DO

PostgreSQL Inheritanced Table Always Return Row With Null Value

I am referring to http://www.if-not-true-then-false.com/2009/11/howto-create-postgresql-table-partitioning-part-1/
To reproduce the problem, here is some simple steps to follow :
(1) create database named "tutorial"
(2) perform the following SQL query :
CREATE TABLE impressions_by_day (
advertiser_id SERIAL NOT NULL,
day DATE NOT NULL DEFAULT CURRENT_DATE,
impressions INTEGER NOT NULL,
PRIMARY KEY (advertiser_id, day)
);
CREATE OR REPLACE FUNCTION insert_table()
RETURNS void AS
$BODY$DECLARE
_impressions_by_day impressions_by_day;
BEGIN
INSERT INTO impressions_by_day(impressions ) VALUES(888) RETURNING * INTO _impressions_by_day;
RAISE NOTICE 'After insert, the returned advertiser_id is %', _impressions_by_day.advertiser_id;
END;$BODY$
LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION insert_table() OWNER TO postgres;
(3) create database named "tutorial_partition"
(4) perform the following SQL query :
CREATE TABLE impressions_by_day (
advertiser_id SERIAL NOT NULL,
day DATE NOT NULL DEFAULT CURRENT_DATE,
impressions INTEGER NOT NULL,
PRIMARY KEY (advertiser_id, day)
);
CREATE OR REPLACE FUNCTION insert_table()
RETURNS void AS
$BODY$DECLARE
_impressions_by_day impressions_by_day;
BEGIN
INSERT INTO impressions_by_day(impressions ) VALUES(888) RETURNING * INTO _impressions_by_day;
RAISE NOTICE 'After insert, the returned advertiser_id is %', _impressions_by_day.advertiser_id;
END;$BODY$
LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION insert_table() OWNER TO postgres;
CREATE TABLE impressions_by_day_y2010m1ms2 (
PRIMARY KEY (advertiser_id, day),
CHECK ( day >= DATE '2010-01-01' AND day < DATE '2010-03-01' )
) INHERITS (impressions_by_day);
CREATE INDEX impressions_by_day_y2010m1ms2_index ON impressions_by_day_y2010m1ms2 (day);
CREATE OR REPLACE FUNCTION impressions_by_day_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF ( NEW.day >= DATE '2010-01-01' AND NEW.day < DATE '2010-03-01' ) THEN
INSERT INTO impressions_by_day_y2010m1ms2 VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Date out of range. Something wrong with the impressions_by_day_insert_trigger() function!';
END IF;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER insert_impressions_by_day_trigger
BEFORE INSERT ON impressions_by_day
FOR EACH ROW EXECUTE PROCEDURE impressions_by_day_insert_trigger();
(5) execute
SELECT * FROM insert_table() on tutorial
We get
NOTICE: After insert, the returned advertiser_id is 1
(6) execute
SELECT * FROM insert_table() on tutorial_partition
We get
NOTICE: After insert, the returned advertiser_id is
How is it possible to get advertiser_id is 1 too, in tutorial_partition?
The trigger is passing back NULL, which indicates to the INSERT that no action is to be performed. Since no action is performed, the RETURNING * clause returns nothing. You're not going to be able to intercept (and override) the INSERT and use RETURNING in the same operation and get anything meaningful.