Trigger using adjusted OLD / NEW values - postgresql

I am trying to put together trigger which does multiple things:
before any insert updates automatically columns created_date and created_by for the inserted row and also inserts this row with updated values into archive (history) table
before any update updates automatically values edited_date and edited_by for the updated row and inserts this updated row (including updated values edited_date and edited_by) into archive table
before any delete...
How to write efficiently the before (or after) update trigger using adjusted NEW (or OLD) values? Here is my trigger:
create trigger my_trigger
before insert or update or delete on my_table
for each row execute procedure my_function();
And function where I would like to use updated new row:
create function my_function() returns trigger as $$
begin
if (tg_op = 'INSERT') then
return 'something done here';
elsif (tg_op = 'UPDATE') then
update NEW set edited_date = now(), edited_by = current_user;
insert into my_table_hist select NEW.*;
elsif (tg_op = 'DELETE') then
return 'something done here';
end if;
return null;
end;
$$ language plpgsql;
What I mean by this is before update I update NEW row columns to current time and current user, then the table gets updated to this new row with updated values, and finally this row with updated values is inserted also into archive (history) table. But it clearly doesn't work like that, so how should I rewrite this trigger function to make it work?

There is much amiss here:
trigger functions must return a row, typically (a modified) NEW, or NULL to abort the operation
you do not update NEW since it is not a table, but you assign to its columns, like
NEW.created := current_timestamp;
for BEFORE DELETE triggers NEW is NULL, since there is no new version of the row

Related

PostgreSQL trigger: first condition executes but not the second

A trigger works on the first part of a function but not the second.
I'm trying to set up a trigger that does two things:
Update a field - geom - whenever the fields lat or lon are updated, using those two fields.
Update a field - country - from the geom field by referencing another table.
I've tried different syntaxes of using NEW, OLD, BEFORE and AFTER conditions, but whatever I do, I can only get the first part to work.
Here's the code:
CREATE OR REPLACE FUNCTION update_geometries()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
update schema.table a set geom = st_setsrid(st_point(a.lon, a.lat), 4326);
update schema.table a set country = b.name
from reference.admin_layers_0 b where st_intersects(a.geom,b.geom)
and a.pk = new.pk;
RETURN NEW;
END;
$$;
CREATE TRIGGER
geom_update
AFTER INSERT OR UPDATE of lat,lon on
schema.table
FOR EACH STATEMENT EXECUTE PROCEDURE update_geometries();
There is no new on a statement level trigger. (well, there is, but it is always Null)
You can either keep the statement level and update the entire a table, i.e. remove the and a.pk = new.pk, or, if only part of the rows are updated, change the trigger for a row-level trigger and only update the affected rows
CREATE OR REPLACE FUNCTION update_geometries()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.geom = st_setsrid(st_point(NEW.lon, NEW.lat), 4326);
SELECT b.name
INTO NEW.country
FROM reference.admin_layers_0 b
WHERE st_intersects(NEW.geom,b.geom);
RETURN NEW;
END;
$$;
CREATE TRIGGER
geom_update
BEFORE INSERT OR UPDATE of lat,lon on
schema.table
FOR EACH ROW EXECUTE PROCEDURE update_geometries();

PostgreSQL - Trigger on INSERT or UPDATE

I wan't to add a trigger in a PostgreSQL database. This trigger is used to concatenate values of 2 columns to update a 3rd one. I wan't to run it when a row is inserted or updated in the table.
Table
CREATE TABLE IF NOT EXISTS FILE(ID INT NOT NULL PRIMARY KEY, FOLDER TEXT, NAME TEXT, URL TEXT);
Function
CREATE OR REPLACE FUNCTION LINK() RETURNS trigger AS
$$
BEGIN
IF (TG_OP = 'UPDATE') THEN
UPDATE FILE
SET URL = CONCAT(FOLDER, NAME)
WHERE ID = OLD.ID;
ELSIF (TG_OP = 'INSERT') THEN
UPDATE FILE
SET URL = CONCAT(FOLDER, NAME)
WHERE ID = NEW.ID;
END IF;
RETURN NULL;
END
$$
LANGUAGE PLPGSQL;
Trigger
CREATE TRIGGER TRIGGER_LINK
BEFORE INSERT OR UPDATE
ON FILE
FOR EACH ROW
EXECUTE PROCEDURE LINK();
When I insert a value in table like
INSERT INTO FILE VALUES (1, 'C:\', 'doc.pdf');
I have an error message list index out of range because ID number is not yet created and UPDATE query on INSERT can't execute. But if I make an AFTER UPDATE it will run infinitely.
How to run a trigger function on INSERT or UPDATE with a WHERE clause on ID to target only inserted or updated row ? I'm using PostgreSQL 10.14.
Don't use UPDATE to do that. Just assign the value. Also, a BEFORE trigger should not return null because that will abort the operation.
CREATE OR REPLACE FUNCTION LINK() RETURNS trigger AS
$$
BEGIN
new.url := CONCAT(new.FOLDER, new.NAME);
RETURN new; --<< important!
END
$$
LANGUAGE PLPGSQL;

How to use the same trigger function for insert/update/delete triggers avoiding the problem with new and old objects

I am looking for an elegant solution to this situation:
I have created a trigger function that updates the table supply with the sum of some detail rows, whenever a row is inserted or updated on warehouse_supplies.
PostgreSQL insert or update syntax allowed me to share the same function sync_supply_stock() for the insert and update conditions.
However, when I try to wire the after delete condition to the function it cannot be reused (although it is logically valid), for the returning object must be old instead of new.
-- The function I want to use for the 3 conditions (insert, update, delete)
create or replace function sync_supply_stock ()
returns trigger
as $$
begin
-- update the supply whose stock just changed in warehouse_supply with
-- the sum its stocks on all the warehouses.
update supply
set stock = (select sum(stock) from warehouse_supplies where supply_id = new.supply_id)
where supply_id = new.supply_id;
return new;
end;
$$ language plpgsql;
-- The (probably) unnecessary copy of the previous function, this time returning old.
create or replace function sync_supply_stock2 ()
returns trigger
as $$
begin
-- update the supply whose stock just changed in warehouse_supply with
-- the sum its stocks on all the warehouses.
update supply
set stock = (select sum(stock) from warehouse_supplies where supply_id = old.supply_id)
where supply_id = old.supply_id;
return old;
end;
$$ language plpgsql;
-- The after insert/update trigger
create trigger on_warehouse_supplies__after_upsert after insert or update
on warehouse_supplies for each row
execute procedure sync_supply_stock ();
-- The after delete trigger
create trigger on_warehouse_supplies__after_delete after delete
on warehouse_supplies for each row
execute procedure sync_supply_stock2 ();
Am I missing something or is there any fixing to duplicating sync_supply_stock2() as sync_supply_stock2()?
EDIT
For the benefit of future readers, following #bergi answer and discusion, this is a possible factorized solution
create or replace function sync_supply_stock ()
returns trigger
as $$
declare
_supply_id int;
begin
-- read the supply_id column from `new` on insert/update conditions and from `old` on delete conditions
_supply_id = coalesce(new.supply_id, old.supply_id);
-- update the supply whose stock just changed in of_warehouse_supply with
-- the sum its stocks on all the warehouses.
update of_supply
set stock = (select sum(stock) from of_warehouse_supplies where supply_id = _supply_id)
where supply_id = _supply_id;
-- returns `new` on insert/update conditions and `old` on delete conditions
return coalesce(new, old);
end;
$$ language plpgsql;
create trigger on_warehouse_supplies__after_upsert after insert or update
on of_warehouse_supplies for each row
execute procedure sync_supply_stock ();
create trigger on_warehouse_supplies__after_delete after delete
on of_warehouse_supplies for each row
execute procedure sync_supply_stock ();
for the returning object must be old instead of new.
No. The return value is only relevant for BEFORE ROW or INSTEAD OF triggers. From the docs: "The return value of a row-level trigger fired AFTER or a statement-level trigger fired BEFORE or AFTER is always ignored; it might as well be null".
So you can just make your sync_supply_stock trigger function RETURN NULL and it can be used on all operations.

How to apply a update after an inser or update POSTGRESQL Trigger

How to apply an update after an insert or update in POSTGRESQL; I have got a table which has a field lastupdate; I want that field to be set up whenever the row is updated or when it was inserted.
I tried this trigger, but It is not working! HELP!!
CREATE OR REPLACE FUNCTION fn_update_profile()
RETURNS TRIGGER AS $update_profile$
BEGIN
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE' ) THEN
UPDATE profile SET lastupdate=now() where oid=OLD.oid;
RETURN NULL;
ELSEIF (TG_OP = 'DELETE') THEN
RETURN NULL;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$update_profile$ LANGUAGE plpgsql;
Your trigger function can be a lot easier than you had. Keep in mind that PG will do the update or the insert on the original table, you only have to deal with keeping the profile table up-to-date:
CREATE OR REPLACE FUNCTION fn_update_profile()
RETURNS TRIGGER AS $update_profile$
BEGIN
UPDATE profile SET lastupdate = now() WHERE oid = NEW.oid;
RETURN NEW;
END;
$update_profile$ LANGUAGE plpgsql;
The INSERT and UPDATE trigger functions both use the NEW parameter; the INSERT trigger function does not have the OLD parameter. You should always return NEW from the trigger function if successful (or OLD from a DELETE trigger), even if it is an AFTER INSERT OR UPDATE trigger; the whole operation will be rolled back if NULL is returned. If you then define the actual trigger to fire after the insert or update, you should be good:
CREATE TRIGGER tr_update_profile
AFTER INSERT OR UPDATE ON my_table
FOR EACH ROW EXECUTE PROCEDURE fn_update_profile();

FOR EACH STATEMENT trigger example

I've been looking at the documentation of postgresql triggers, but it seems to only show examples for row-level triggers, but I can't find an example for a statement-level trigger.
In particular, it is not quite clear how to iterate in the update/inserted rows in a single statement, since NEW is for a single record.
OLD and NEW are null or not defined in a statement-level trigger. Per documentation:
NEW
Data type RECORD; variable holding the new database row for INSERT/UPDATE operations in row-level triggers. This variable is
null in statement-level triggers and for DELETE operations.
OLD
Data type RECORD; variable holding the old database row for UPDATE/DELETE operations in row-level triggers. This variable is null in statement-level triggers and for INSERT operations.
Bold emphasis mine.
Up to Postgres 10 this read slightly different, much to the same effect, though:
... This variable is unassigned in statement-level triggers. ...
While those record variables are still of no use for statement level triggers, a new feature very much is:
Transition tables in Postgres 10+
Postgres 10 introduced transition tables. Those allow access to the whole set of affected rows. The manual:
AFTER triggers can also make use of transition tables to inspect the entire set of rows changed by the triggering statement.
The CREATE TRIGGER command assigns names to one or both transition
tables, and then the function can refer to those names as though they
were read-only temporary tables. Example 43.7 shows an example.
Follow the link to the manual for code examples.
Example statement-level trigger without transition tables
Before the advent of transition tables, those were even less common. A useful example is to send notifications after certain DML commands.
Here is a basic version of what I use:
-- Generic trigger function, can be used for multiple triggers:
CREATE OR REPLACE FUNCTION trg_notify_after()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
PERFORM pg_notify(TG_TABLE_NAME, TG_OP);
RETURN NULL;
END
$func$;
-- Trigger
CREATE TRIGGER notify_after
AFTER INSERT OR UPDATE OR DELETE ON my_tbl
FOR EACH STATEMENT
EXECUTE PROCEDURE trg_notify_after();
For Postgres 11 or later use the equivalent, less confusing syntax:
...
EXECUTE FUNCTION trg_notify_after();
See:
Trigger function does not exist, but I am pretty sure it does
Well, here are some examples of statement-level triggers.
Table:
CREATE TABLE public.test (
number integer NOT NULL,
text character varying(50)
);
Trigger function:
OLD and NEW are still NULL
The return value can also be always left NULL.
CREATE OR REPLACE FUNCTION public.tr_test_for_each_statement()
RETURNS trigger
LANGUAGE plpgsql
AS
$$
DECLARE
x_rec record;
BEGIN
raise notice '=operation: % =', TG_OP;
IF (TG_OP = 'UPDATE' OR TG_OP = 'DELETE') THEN
FOR x_rec IN SELECT * FROM old_table LOOP
raise notice 'OLD: %', x_rec;
END loop;
END IF;
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
FOR x_rec IN SELECT * FROM new_table LOOP
raise notice 'NEW: %', x_rec;
END loop;
END IF;
RETURN NULL;
END;
$$;
Settings statement-level triggers
Only AFTER and only one event is supported.
CREATE TRIGGER tr_test_for_each_statement_insert
AFTER INSERT ON public.test
REFERENCING NEW TABLE AS new_table
FOR EACH STATEMENT
EXECUTE PROCEDURE public.tr_test_for_each_statement();
CREATE TRIGGER tr_test_for_each_statement_update
AFTER UPDATE ON public.test
REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table
FOR EACH STATEMENT
EXECUTE PROCEDURE public.tr_test_for_each_statement();
CREATE TRIGGER tr_test_for_each_statement_delete
AFTER DELETE ON public.test
REFERENCING OLD TABLE AS old_table
FOR EACH STATEMENT
EXECUTE PROCEDURE public.tr_test_for_each_statement();
Examples:
INSERT INTO public.test(number, text) VALUES (1, 'a');
=operation: INSERT =
NEW: (1,a)
INSERT INTO public.test(number, text) VALUES (2, 'b'), (3, 'b');
=operation: INSERT =
NEW: (2,b)
NEW: (3,b)
UPDATE public.test SET number = number + 1 WHERE text = 'a';
=operation: UPDATE =
OLD: (1,a)
NEW: (2,a)
UPDATE public.test SET number = number + 10 WHERE text = 'b';
=operation: UPDATE =
OLD: (2,b)
OLD: (3,b)
NEW: (12,b)
NEW: (13,b)
DELETE FROM public.test;
=operation: DELETE =
OLD: (2,a)
OLD: (12,b)
OLD: (13,b)