How can a Postgres trigger `AFTER UPDATE FOR EACH STATEMENT` correlate its `OLD TABLE` and `NEW TABLE`? - sql

After upgrading from Postgres 9.5 to 11, I am trying to replace the following FOR EACH ROW trigger with a FOR EACH STATEMENT trigger which I hope would be more efficient for my specific use-case:
CREATE OR REPLACE FUNCTION audit_update_operations()
RETURNS TRIGGER
AS
$$
DECLARE
audit_user UUID;
data_before TEXT;
data_after TEXT;
BEGIN
audit_user := coalesce(current_setting('audit.AUDIT_USER', TRUE), '77777777-0000-7777-0000-777777777777')::UUID;
data_before := ROW (old.*);
data_after := ROW (new.*);
INSERT INTO dml_audit_log
(changed_at,
user_id,
operation,
table_name,
data_after,
data_before)
VALUES (now(),
audit_user,
'U',
tg_table_name::TEXT,
data_after,
data_before);
RETURN new;
END ;
$$
LANGUAGE plpgsql;
I thought this was pretty easy:
CREATE OR REPLACE FUNCTION audit_update_operations()
RETURNS TRIGGER
AS
$$
DECLARE
user_id UUID;
BEGIN
user_id := coalesce(current_setting('audit.AUDIT_USER', TRUE), '77777777-0000-7777-0000-777777777777')::UUID;
INSERT INTO dml_audit_log
SELECT now() AS changed_at,
user_id,
'U' AS operation,
tg_table_name::TEXT AS table_name,
ROW (new_table.*) AS data_after,
ROW (old_table.*) AS data_before
FROM new_table;
RETURN NULL;
END ;
$$
LANGUAGE plpgsql;
which is called generically for every table via the following trigger:
EXECUTE ('
CREATE TRIGGER trigger_audit_update
AFTER UPDATE ON ' || tablename || '
REFERENCING
OLD TABLE AS old_table
NEW TABLE AS new_table
FOR EACH STATEMENT
EXECUTE PROCEDURE audit_update_operations()');
but an UPDATE statement triggering this function results in the following error:
[42P01] ERROR: missing FROM-clause entry for table "old_table" Where: PL/pgSQL function shared.audit_update_operations() line 7 at SQL statement
The question I am struggling with is:
How can I correlate the old and new rows without making any assumption about the table being changed?
The tables I am triggering on may or may not have a primary key. Even if they do, I would not know its name or column(s) in the trigger function.
Are the rows in OLD TABLE and NEW TABLE guaranteed to be in the same order? I cannot rely on undocumented implementation details that may change.

Related

Error: Relation "abc.emp_audit" does not exist. when i add schema name in all objects. it was working fine without schema

CREATE TABLE abc.emp
(
empname TEXT NOT NULL,
salary INTEGER
);
CREATE TABLE abc.emp_audit
(
operation CHAR(1) NOT NULL,
stamp TIMESTAMP NOT NULL,
userid TEXT NOT NULL,
empname TEXT NOT NULL,
salary INTEGER
);
CREATE FUNCTION abc.audit()
returns TRIGGER AS $$ ...
EXECUTE format
( 'INSERT INTO %I SELECT ''I'', current_timestamp, %L, ($1::%I.%I).*',
tg_argv[0], CURRENT_USER, tg_table_schema, tg_table_name ) using new;
... $$;
CREATE TRIGGER emp_audit_trig after
INSERT
OR
UPDATE
OR
DELETE
ON abc.emp FOR each row
EXECUTE FUNCTION
abc.audit('abc.emp_audit');
I got error : Relation "abc.emp_audit" does not exist . How to correct this issue when schema added in tables ad audit tables, functions, and triggers arguments.
You'd have to use two parameters, one for the schema and one for the table. In the trigger function, use
EXECUTE
format(
'INSERT INTO %I.%I SELECT ''I'', current_timestamp, %L, ($1::%I.%I).*',
TG_ARGV[0], TG_ARGV[1],
current_user,
TG_TABLE SCHEMA, TG_TABLE NAME
) USING NEW;
Then define the trigger as
CREATE TRIGGER ... EXECUTE FUNCTION abc.audit('abc', 'emp_audit');
An alternative to the one proposed by #Laurenz Albe is to stick to a single parameter and then split it before creating the INSERT statement, e.g
CREATE FUNCTION abc.audit() RETURNS TRIGGER AS $$
DECLARE
tbname TEXT := (string_to_array(tg_argv[0],'.'))[1];
schname TEXT := (string_to_array(tg_argv[0],'.'))[2];
BEGIN
-- rest of your code ...
EXECUTE format('INSERT INTO %I.%I SELECT ''I'', current_timestamp, %L, ($1::%I.%I).*',
tbname,schname,CURRENT_USER,tg_table_schema,tg_table_name ) USING NEW;
-- rest of your code ...
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER emp_audit_trig AFTER INSERT OR UPDATE OR DELETE
ON abc.emp FOR EACH ROW EXECUTE FUNCTION abc.audit('abc.emp_audit');
Demo: db<>fiddle

Modify OLD to be returned by DELETE in postgresql trigger function

I have a trigger function in postgresql which will insert rows in the audit table on INSERT, UPDATE and DELETE operations. In my tables, there is a column called audit_id and I need to write the ID of inserted audit row in this field. This is my function
CREATE OR REPLACE FUNCTION my_audit_trigger()
RETURNS trigger LANGUAGE plpgsql
AS $function$
declare
audit_pk bigint;
begin
IF TG_OP = 'INSERT'
THEN
INSERT INTO audit.table_audit (rel_id, table_name, operation, after)
VALUES (TG_RELID, TG_TABLE_NAME, TG_OP, to_jsonb(NEW)) returning id into audit_pk;
NEW.audit_id := audit_pk;
RETURN NEW;
ELSIF TG_OP = 'UPDATE'
THEN
IF NEW != OLD THEN
INSERT INTO audit.table_audit (rel_id, table_name, operation, before, after)
VALUES (TG_RELID, TG_TABLE_NAME, TG_OP, to_jsonb(OLD), to_jsonb(NEW)) returning id into audit_pk;
END IF;
NEW.audit_id := audit_pk;
RETURN NEW;
ELSIF TG_OP = 'DELETE'
THEN
INSERT INTO audit.table_audit (rel_id, table_name, operation, before)
VALUES (TG_RELID, TG_TABLE_NAME, TG_OP, to_jsonb(OLD)) returning id into audit_pk;
OLD.audit_id := audit_pk;
RETURN OLD;
END IF;
end;
$function$;
As a result, when inserting or updating my table rows, I get back the audit id of the corresponding operation, but when I run DELETE command, I get back the audit ID of the previous operation, not of the DELETE itself. So I guess the problem is in OLD.audit_id := audit_pk;
More specifically, I run for example INSERT INTO table VALUES (this, that) RETURNING audit_id and I get back audit_id of the INSERT operation.
After, when running DELETE FROM table WHERE id = sth RETURNING audit_id I get audit_id of the INSERT operation, not of the DELETE.
Any help is appreciated, thank you.
P.S. This is how I create trigger
CREATE TRIGGER table_trigger
BEFORE INSERT OR UPDATE OR DELETE
ON table
FOR EACH ROW
EXECUTE PROCEDURE my_audit_trigger();
I have similar problem. That seems that PG just does not support modification of OLD now, but, probably, this feature will be included into TODO list.
Currently you can modify only NEW for INSERT and UPDATE statements
For details look into this mail thread: Does 'instead of delete' trigger support modification of OLD

PL/PGSQL dynamic trigger for all tables in schema

I am looking to automate each table update with an automatic update of the updated_at column. I am able to make this work for a specific table using a trigger. But my main goal, which I can't find anywhere, is to create a function that dynamically grabs all the tables in the schema, and creates that same trigger and only changing the table name that the trigger is referencing to. For the life of me I can't figure it out.
I believe this shouldn't be as tricky as I'm making it as ever table in our schema will have the exact same column name of 'updated_at'.
One solution that I tried and thought would work was turning the table schema into an array, and iterating through that to invoke/create the trigger each iteration. But I don't have a ton of psql experience so I am finding myself googling for hours to solve this one little thing.
SELECT ARRAY (
SELECT
table_name::text
FROM
information_schema.tables
WHERE table_schema = 'public') as tables;
I have also tried:
DO $$
DECLARE
t text;
BEGIN
FOR t IN
SELECT table_name FROM information_schema.columns
WHERE column_name = 'updated_at'
LOOP
EXECUTE format('CREATE TRIGGER update_updatedAt
BEFORE UPDATE ON %I
FOR EACH ROW EXECUTE PROCEDURE updated_at()',
t);
END loop;
END;
$$ language 'plpgsql';
Procedure:
CREATE OR REPLACE FUNCTION updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
Your DO block works. The only problem with it is we can't have same Trigger name for multiple triggers. So, you can either add a table_name suffix/prefix for the Trigger name.
DO $$
DECLARE
t text;
BEGIN
FOR t IN
SELECT table_name FROM information_schema.columns
WHERE column_name = 'updated_at'
LOOP
EXECUTE format('CREATE TRIGGER update_updatedAt_%I
BEFORE UPDATE ON %I
FOR EACH ROW EXECUTE PROCEDURE updated_at()',
t,t);
END loop;
END;
$$ language 'plpgsql';
Additionally you may add a check to see if the trigger already exists in information_schema.triggers to be safe.
IF NOT EXISTS ( SELECT 1 from information_schema.triggers
where trigger_name = 'update_updatedat_'|| t)
THEN

PostgreSQL trigger after update only on a updated row

I have a small table for news. I want to make a trigger which sets the update date and update time in the row (only for the rows that were updated)
I tried making the following:
CREATE FUNCTION add_update_dates()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF (OLD.news_name IS DISTINCT FROM NEW.news_name OR
OLD.news_description IS DISTINCT FROM NEW.news_description OR
OLD.news_text IS DISTINCT FROM NEW.news_text) THEN
UPDATE news SET news_update_date = current_date, news_update_time = current_time;
END IF;
RETURN new;
END
$$;
CREATE TRIGGER update_news_dates
AFTER UPDATE ON news
FOR EACH ROW
EXECUTE PROCEDURE add_update_dates();
But the trigger updates each row in my table (even those that are not updated), when I want only the updated ones. What am I doing wrong?
Your update statement is updating all the rows in the table! It has no where clause.
Just use assignment:
CREATE FUNCTION add_update_dates()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF (OLD.news_name IS DISTINCT FROM NEW.news_name OR
OLD.news_description IS DISTINCT FROM NEW.news_description OR
OLD.news_text IS DISTINCT FROM NEW.news_text
) THEN
NEW.news_update_date := current_date;
NEW.news_update_time := current_time;
END IF;
RETURN new;
END;
$$;
As an aside, storing date/time in separate columns makes no sense to me.

Postgresql delete record involving two tables and store this record in the third record

I'm trying to delete a record from the table 'student', where on a Cascade delete it will remove it from the 'entry' table. But before delete i need to store this record in the third table 'cancel'.
Here is what i worked out so far:
DELETE FROM "CMPS".student
WHERE sno = '1';
CREATE TRIGGER canceled BEFORE DELETE
ON entry
FOR EACH ROW
EXECUTE PROCEDURE trigger_backup_row
CREATE OR REPLACE FUNCTION trigger_backup_row(integer)
RETURNS trigger AS
$$
BEGIN
INSERT INTO cancel (eno, excode, sno) values (NEW.eno, NEW.excode, NEW.sno);
RETURN NEW;
END;
$$
language PLPGSQL
But comes back with an errors. Any help will be much appreciated.
I suppose you need:
CREATE OR REPLACE FUNCTION trigger_backup_row()
RETURNS trigger AS
$$
BEGIN
INSERT INTO cancel (eno, excode, sno) values (OLD.eno, OLD.excode, OLD.sno);
RETURN OLD;
END;
$$
language PLPGSQL
;
CREATE TRIGGER canceled BEFORE DELETE
ON entry
FOR EACH ROW
EXECUTE PROCEDURE trigger_backup_row()
;
trigger function do not use arguments
on delete yo udon't have any NEW row - just an OLD one