How to DELETE/INSERT rows in the same table using a UPDATE Trigger? - postgresql

I want to create a trigger function, which copies certain columns of an recent updated row and deletes the old data. After that I want to insert the copied columns in exact the same table in the same row (overwrite). I need the data to be INSERTED because this function will be embedded in an existing program, with predefined Triggers.
That's what I have so far:
CREATE OR REPLACE FUNCTION update_table()
RETURNS TRIGGER AS
$func$
BEGIN
WITH tmp AS (DELETE FROM table
WHERE table.id = NEW.id
RETURNING id, geom )
INSERT INTO table (id, geom) SELECT * FROM tmp;
END;
$func$ language plpgsql;
CREATE TRIGGER T_update
AFTER UPDATE OF geom ON table
EXECUTE PROCEDURE update_table();
But I get the Error message:
ERROR: cannot perform DELETE RETURNING on relation "table"
HINT: You need an unconditional ON DELETE DO INSTEAD rule with a RETURNING clause.
Why I should use a rule here?
I'm using PostgreSQL 9.6
UPDATE:
A little bit of clarification. When I have two columns in my table (id, geom), after I updated geom I want to make a copy of this (new)row and insert it into the same table, while overwriting the updated row. (I'm not interested in any value before the update) I know that this is odd but I need this row to be inserted again because the program i embed this function in, listens to a INSERT statement and cannot be changed by me.

Right after you update a row, its old values will no longer be available. So, if you simply want to preserve the old row in case of an update you need to create a BEFORE UPDATE trigger, so that you can still access the OLD values and create a new row, e.g.
CREATE TABLE t (id int, geom geometry(point,4326));
CREATE OR REPLACE FUNCTION update_table() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO t (id, geom) VALUES (OLD.id,OLD.geom);
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER t_update
BEFORE UPDATE OF geom ON t FOR EACH ROW EXECUTE PROCEDURE update_table();
INSERT INTO t VALUES (1,'SRID=4326;POINT(1 1)');
If you update the record 1 ..
UPDATE t SET geom = 'SRID=4326;POINT(2 2)', id = 2 WHERE id = 1;
UPDATE t SET geom = 'SRID=4326;POINT(3 3)', id = 3 WHERE id = 2;
.. you get a new record in the same table as you wished
SELECT id, ST_AsText(geom) FROM t;
id | st_astext
----+------------
1 | POINT(1 1)
2 | POINT(2 2)
3 | POINT(3 3)
Demo: db<>fiddle
Unrelated note: consider upgrading your PostgreSQL version! 9.6 will reach EOL in November, 2021.

First thanks to #JimJones for the answer. I´d like to post his answer modified for this purpose. This code "overwrites" the updated row by inserting a copy of itself and then deleting the old duplicate. That way I can Trigger on INSERT.
CREATE TABLE t (Unique_id SERIAL,id int, geom geometry(point,4326));
CREATE OR REPLACE FUNCTION update_table() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO t (id, geom) VALUES (NEW.id,NEW.geom);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER t_update
BEFORE UPDATE OF geom ON t FOR EACH ROW EXECUTE PROCEDURE update_table();
CREATE OR REPLACE FUNCTION delete_table() RETURNS TRIGGER AS $$
BEGIN
DELETE FROM t a
USING t b
WHERE a.Unique_id < b.Unique_id
AND a.geom = b.geom;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER t_delete
AFTER UPDATE OF geom ON t FOR EACH ROW EXECUTE PROCEDURE delete_table();
INSERT INTO t VALUES (1,1,'SRID=4326;POINT(1 1)');
UPDATE t SET geom = 'SRID=4326;POINT(2 2)' WHERE id = 1;

Related

New columns can't be updated if a trigger BEFORE UPDATE is triggered

I came across a strange behavior (at least for me) with PostgreSQL and a trigger BEFORE UPDATE.
I have a table witch has an updated_at column witch is set by a BEFORE UPDATE trigger.
I need to add new columns to this table and set their values with an UPDATE query (not with DEFAULT).
It works just fine excepts when i do an UPDATE juste before adding those columns.
Here's an example :
ALTER TABLE my_schema.my_table ADD COLUMN new_column varchar(50);
UPDATE my_schema.my_table SET new_column = 'new_column_update' WHERE id = xxxxxx;
This script works fine.
But if i do an UPDATE before :
UPDATE my_schema.my_table SET other_column = 'other_column_update' WHERE id = xxxxxx; -- the TRIGGER is triggered
ALTER TABLE my_schema.my_table ADD COLUMN new_column varchar(50);
UPDATE my_schema.my_table SET new_column = 'new_column_update' WHERE id = xxxxxx; -- this UPDATE does't do anything
It doesn't works anymore.
After a few (a lot) hours, i found that the trigger BEFORE UPDATE is reponsible. But i can't find why.
I found a workaround by temporary disabling the trigger
ALTER TABLE my_table DISABLE TRIGGER update_date;
Here is a dbfiddle, just run it to see this behaviour :
dbfiddle
Here is the code in dbfiddle
CREATE TABLE my_table (
other_column varchar(50),
updated_at timestamp
);
CREATE OR REPLACE FUNCTION update_date()
RETURNS trigger
LANGUAGE plpgsql
COST 1
AS '
BEGIN
IF row(NEW.*) IS DISTINCT FROM row(OLD.*) THEN
NEW.updated_at = now();
RETURN NEW;
ELSE
RETURN OLD;
END IF;
END;
'
;
CREATE TRIGGER update_date BEFORE
UPDATE
ON
my_table FOR EACH ROW EXECUTE PROCEDURE update_date();
INSERT INTO my_table VALUES ('other_column_insert');
UPDATE my_table SET other_column = 'other_column_update';
ALTER TABLE my_table ADD COLUMN new_colum varchar(50);
UPDATE my_table SET new_colum = 'new_colum_update'; -- this UPDATE doesn't work because of the trigger BEFORE UPDATE
-- It is possible to make it works by disabling the trigger BEFORE the first UPDATE
-- ALTER TABLE my_table DISABLE TRIGGER update_date;
Have you ever encountered this behavior ?
It's something to do with the (unnecessary) wrapping of NEW/OLD with a ROW(...) constructor:
BEGIN
IF row(NEW.*) IS DISTINCT FROM row(OLD.*) THEN
-- IF NEW IS DISTINCT FROM OLD THEN
NEW.updated_at = now();
ELSE
RAISE EXCEPTION $$NOT DISTINCT: % / %$$, NEW, OLD;
END IF;
RETURN NEW;
END;
I've also moved the RETURN NEW to the end. If you try your version you should see the exceptions. If you replace it out with the commented-out one below then it works.
Now, as to why this is failing when you compare rows I'm not sure and it's too hot and late on a Friday afternoon where I am to figure it out I'm afraid.
I am going to say this is a caching problem. I modified the function to see what is going on:
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table (
other_column varchar(50),
updated_at timestamp
);
CREATE OR REPLACE FUNCTION public.update_date()
RETURNS trigger
LANGUAGE plpgsql
COST 1
AS $function$
BEGIN
RAISE NOTICE 'New row %', ROW(NEW.*);
RAISE NOTICE 'Old row%', ROW(OLD.*);
RAISE NOTICE 'New.* %', (NEW.*)::text;
RAISE NOTICE 'Old.* %', (OLD.*)::text;
IF NEW.* IS DISTINCT FROM OLD.* THEN
NEW.updated_at = now();
RETURN NEW;
ELSE
RETURN OLD;
END IF;
END;
$function$;
CREATE TRIGGER update_date BEFORE
UPDATE
ON
my_table FOR EACH ROW EXECUTE PROCEDURE update_date();
INSERT INTO my_table VALUES ('other_column_insert');
UPDATE my_table SET other_column = 'other_column_update';
NOTICE: New row (other_column_update,)
NOTICE: Old row(other_column_insert,)
NOTICE: New.* (other_column_update,)
NOTICE: Old.* (other_column_insert,)
ALTER TABLE my_table ADD COLUMN new_colum varchar(50);
UPDATE my_table SET new_colum = 'new_colum_update';
NOTICE: New row (other_column_update,"2022-08-12 10:38:54.815831")
NOTICE: Old row(other_column_update,"2022-08-12 10:38:54.815831")
NOTICE: New.* (other_column_update,"2022-08-12 10:38:54.815831",new_colum_update)
NOTICE: Old.* (other_column_update,"2022-08-12 10:38:54.815831",)
It has to do with the ROW(). Even doing ROW(NEW.*)::my_table or using EXECUTE to make the query dynamic and not use caching does not work.

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 to insert rows

I'm stuck for days with triggers on Postgresql (and Mysql as well). I just want to insert newly filled rows to another table. The original data comes from an external form (OpenDataKit) and goes to "intermediate" tables. I can't understand why the form cannot send the data anymore once the trigger is created... Note that all actions work without the trigger, when I do the insertions by hand. I would greatly appreciate some help to understand what I am doing wrong. I am now testing with Postgresql 9.5, but I got similar issue with MySQL 5.1.
-- CREATE procedure:
CREATE OR REPLACE FUNCTION proc_natobs() RETURNS TRIGGER AS
$BODY$
DECLARE
BEGIN
INSERT INTO lieu (id_lieu, wgs_lat, wgs_lon, date_obs, geom)
SELECT id_loc,"GPS_TEL_LAT", "GPS_TEL_LNG", "DATE_OBS", ST_SetSRID(ST_POINT("GPS_TEL_LNG","GPS_TEL_LAT"), 4326)
FROM "FORMULAIRE_NATOBS_REPEAT_LOC", "FORMULAIRE_NATOBS_CORE"
WHERE "FORMULAIRE_NATOBS_CORE"."_URI" = "FORMULAIRE_NATOBS_REPEAT_LOC"."_TOP_LEVEL_AURI"
AND "FORMULAIRE_NATOBS_REPEAT_LOC".id_loc IN (SELECT max(id_loc) FROM "FORMULAIRE_NATOBS_REPEAT_LOC");
INSERT INTO i_lieu_observateurs (id_lieu, id_auteur)
SELECT id_loc, CAST("AUTEUR" AS integer)
FROM "FORMULAIRE_NATOBS_CORE", "FORMULAIRE_NATOBS_REPEAT_LOC"
WHERE "FORMULAIRE_NATOBS_REPEAT_LOC"."_TOP_LEVEL_AURI" = "FORMULAIRE_NATOBS_CORE"."_URI"
AND id_loc IN (SELECT max(id_loc) FROM "FORMULAIRE_NATOBS_REPEAT_LOC")
UNION
SELECT id_loc, CAST("OBSERVATEURS" AS integer)
FROM "FORMULAIRE_NATOBS_REPEAT_LOC", "FORMULAIRE_NATOBS_REPEAT_OBSERVATEUR"
WHERE "FORMULAIRE_NATOBS_REPEAT_LOC"."_TOP_LEVEL_AURI" = "FORMULAIRE_NATOBS_REPEAT_OBSERVATEUR"."_TOP_LEVEL_AURI"
AND id_loc IN (SELECT max(id_loc) FROM "FORMULAIRE_NATOBS_REPEAT_LOC")
;
END;
$BODY$
LANGUAGE 'plpgsql';
-- CREATE the trigger:
CREATE TRIGGER trigger_natobs AFTER INSERT
ON "FORMULAIRE_NATOBS_REPEAT_LOC"
FOR EACH ROW
EXECUTE PROCEDURE proc_natobs();
So, when the ODK form inserts new rows in FORMULAIRE_NATOBS_REPEAT_LOC (for which I have created a serial ID to facilitate the SQL queries), I try to insert this row (combined with information from other intermediate tables) into table "lieu" for the first action of the trigger, and into table i_lieu_observation (composed by a double primary key) for the second action. I tested also with a trigger composed by the first action only, but it does not work either. The Android app that sends the form crashes until I remove the trigger.
Thanks in advance!
You need to use the special NEW variable in the trigger to access the newly inserted data. So you need something like:
CREATE OR REPLACE FUNCTION proc_natobs() RETURNS TRIGGER AS
$BODY$
DECLARE
BEGIN
INSERT INTO lieu (id_lieu, wgs_lat, wgs_lon, date_obs, geom)
SELECT new.id_loc,"GPS_TEL_LAT", "GPS_TEL_LNG", "DATE_OBS", ST_SetSRID(ST_POINT("GPS_TEL_LNG","GPS_TEL_LAT"), 4326)
FROM "FORMULAIRE_NATOBS_CORE"
WHERE "FORMULAIRE_NATOBS_CORE"."_URI" = new."_TOP_LEVEL_AURI";
INSERT INTO i_lieu_observateurs (id_lieu, id_auteur)
SELECT new.id_loc, CAST("AUTEUR" AS integer)
FROM "FORMULAIRE_NATOBS_CORE"
WHERE new."_TOP_LEVEL_AURI" = "FORMULAIRE_NATOBS_CORE"."_URI"
UNION
SELECT new.id_loc, CAST("OBSERVATEURS" AS integer)
FROM "FORMULAIRE_NATOBS_REPEAT_OBSERVATEUR"
WHERE new."_TOP_LEVEL_AURI" = "FORMULAIRE_NATOBS_REPEAT_OBSERVATEUR"."_TOP_LEVEL_AURI";
RETURN new;
END;
$BODY$
LANGUAGE 'plpgsql';
-- CREATE the trigger:
CREATE TRIGGER trigger_natobs AFTER INSERT
ON "FORMULAIRE_NATOBS_REPEAT_LOC"
FOR EACH ROW
EXECUTE PROCEDURE proc_natobs();
Because I don't know which fields come from which tables, I cannot make the above totally correct. In the same way as I have written new.id_loc, you will need to put new.field_name for all fields coming from the formulaire_natobs_repeat_loc table.
HTH
Try this
CREATE OR REPLACE FUNCTION proc_natobs() RETURNS TRIGGER AS
$BODY$
BEGIN
IF(TG_OP = 'INSERT') THEN
INSERT INTO lieu (id_lieu, wgs_lat, wgs_lon, date_obs, geom)
SELECT id_loc,"GPS_TEL_LAT", "GPS_TEL_LNG", "DATE_OBS", ST_SetSRID(ST_POINT("GPS_TEL_LNG","GPS_TEL_LAT"), 4326)
FROM "FORMULAIRE_NATOBS_REPEAT_LOC" loc, "FORMULAIRE_NATOBS_CORE" core
WHERE core."_URI" = loc."_TOP_LEVEL_AURI"
AND loc.id_loc =new.id_loc;
INSERT INTO i_lieu_observateurs (id_lieu, id_auteur)
SELECT id_loc as id,
CAST("AUTEUR" AS integer) as auteur
FROM "FORMULAIRE_NATOBS_CORE" core, "FORMULAIRE_NATOBS_REPEAT_LOC" loc
WHERE loc."_TOP_LEVEL_AURI" = core."_URI"
AND loc.id_loc =new.id_loc;
UNION
SELECT id_loc as id,
CAST("OBSERVATEURS" AS integer) as auteur
FROM "FORMULAIRE_NATOBS_REPEAT_LOC" loc, "FORMULAIRE_NATOBS_REPEAT_OBSERVATEUR" obs
WHERE loc."_TOP_LEVEL_AURI" = obs."_TOP_LEVEL_AURI"
AND loc.id_loc =new.id_loc;
END IF;
Return new;
END;
$BODY$
LANGUAGE 'plpgsql';
-- CREATE the trigger:
CREATE TRIGGER trigger_natobs AFTER INSERT
ON "FORMULAIRE_NATOBS_REPEAT_LOC"
FOR EACH ROW
EXECUTE PROCEDURE proc_natobs();
Hope it work for you.

Trigger insert into another table only if unique value

I have a trigger function that copy row of unique values to another table on update or insert that ALMOST work.
The trigger should only insert a new row to the sample table if the number don't exist in it before. Atm. it insert a new row to the sample table with the value NULL if the number already exist in the table. I dont want it to do anything if maintbl.number = sample.nb_main
EDIT: sample table and sample data
CREATE TABLE schema.main(
sid SERIAL NOT NULL,
number INTEGER,
CONSTRAINT sid_pk PRIMARY KEY (sid)
)
CREATE TABLE schema.sample(
gid SERIAL NOT NULL,
nb_main INTEGER,
CONSTRAINT gid_pk PRIMARY KEY (gid)
Example and desired result
schema.main schema.sample
number nb_main
234233 234233
234234 555555
234234
555555
555555
CREATE OR REPLACE FUNCTION schema.update_number()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO schema.sample(
nb_main)
SELECT DISTINCT(maintbl.number)
FROM schema.maintbl
WHERE NOT EXISTS (
SELECT nb_main FROM schema.sample WHERE maintbl.number = sample.nb_main);
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION schema.update_number()
OWNER TO postgres;
CREATE TRIGGER update_number
AFTER INSERT OR UPDATE
ON schema.maintbl
FOR EACH ROW
EXECUTE PROCEDURE schema.update_number();
I just found out that my select query is probably wrong, if I run SELECT query by itself it return one row 'NULL' but i should not?
SELECT DISTINCT(maintbl.number)
FROM schema.maintbl
WHERE NOT EXISTS (
SELECT nb_main FROM schema.sample WHERE maintbl.number = sample.nb_main);
Any good advice?
Best
If I understood correctly, you wish to append to schema.sample a number that has been inserted or updated in schema.maintbl, right?
CREATE OR REPLACE FUNCTION schema.update_number()
RETURNS trigger AS
$BODY$
BEGIN
IF (SELECT COUNT(*) FROM schema.sample WHERE number = NEW.number) = 0 THEN
INSERT INTO schema.sample(nb_main) VALUES (NEW.number);
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;

Postgres triggers - adding old and new values

I am trying to learn Postgres triggers, using some simple examples. I have created a simple table:
create table emp (empname text, salary integer, last_user text);
My goal is to replace the old salary with a new salary computed as the salary inserted (new) + the old salary. I could not get them to sum even when I did not put a condition (i.e. empname is the same)
Here is my code:
-- this table returns a new row instead of summing
CREATE OR REPLACE FUNCTION emp_stamp() RETURNS trigger
AS $emp_stamp$
BEGIN
new.salary = new.salary + old.salary ;
RETURN NEW;
END;
$emp_stamp$ LANGUAGE plpgsql;
CREATE TRIGGER emp_stamp
BEFORE UPDATE on emp
FOR EACH ROW
EXECUTE PROCEDURE emp_stamp();
When I insert into the table, I get a new row added and no existing rows follow the formula:
INSERT INTO EMP VALUES('BR',39970,'BR')
I have also unsuccessfully tried the UPDATE command.
Your CREATE TRIGGER script says BEFORE UPDATE. So this trigger is not fired at all for INSERT commands.
Moreover, the same trigger function would raise an error for INSERT commands anyway because, obviously, there is no "old" version for newly inserted rows.
It should work just fine as is for UPDATE, though. I just cleaned it up a bit:
CREATE OR REPLACE FUNCTION emp_stamp()
RETURNS trigger AS
$func$
BEGIN
NEW.salary := NEW.salary + OLD.salary;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER emp_stamp
BEFORE UPDATE ON emp
FOR EACH ROW EXECUTE PROCEDURE emp_stamp();
Just a proof of concept, I fail to see the point of adding up old an new value.
For starters, your table needs a proper PRIMARY KEY, a serial column for instance (empname is hardly unique):
CREATE TABLE emp (
emp_id serial PRIMARY KEY
, empname text
, salary integer
, last_user text);
Then the UPDATE could work reliably:
UPDATE EMP
SET salary = 39970
WHERE emp_id = 123;
I still don't see how the mentioned trigger would make sense. You could increase an existing salary like this, no trigger involved:
UPDATE EMP
SET salary = salary + 39970
WHERE emp_id = 123;