PostgreSQL subquery with IF EXISTS in trigger function - postgresql

I have a PostgreSQL trigger function like so:
CREATE FUNCTION playlists_tld_update_trigger() RETURNS TRIGGER AS
$$
BEGIN
IF EXISTS (SELECT 1 FROM "subjects" WHERE "subjects"."id" = new.subject_id) THEN
new.tld = (SELECT "subjects"."tld" FROM "subjects" WHERE "subjects"."id" = new.subject_id LIMIT 1);
END IF;
RETURN new;
END
$$
LANGUAGE plpgsql;
The trigger function will set the playlist's "tld" column to match the subject's "tld" column, but only if there exists a subject referenced by the subject_id foreign key. How do I use a subquery to combine the 2 queries into 1, or to avoid redundancy?

CREATE FUNCTION playlists_tld_update_trigger()
RETURNS TRIGGER
AS $$
DECLARE
my_tld <data_type_of_tld>;
BEGIN
SELECT subjects.tld
INTO my_tld
FROM subjects
WHERE subjects.id = new.subject_id
LIMIT 1
;
IF FOUND
THEN
new.tld = my_tld;
RETURN NEW;
ELSE
-- do something else
RETURN OLD;
END IF;
END
$$ LANGUAGE plpgsql;

Related

Postgres update trigger

I've problem with a trigger function in postgresql.
Here my simple code.
CREATE TABLE specie
(specie_id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
nome_comune TEXT UNIQUE,
nome_scientifico TEXT UNIQUE);
CREATE TABLE rilevatore
(rilevatore_id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
nome_cognome TEXT);
CREATE TABLE evento_investimento
(evento_id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
data DATE,
ora TIME WITHOUT TIME ZONE,
rilevatore_id INT REFERENCES rilevatore (rilevatore_id),
specie_id INT REFERENCES specie(specie_id));
CREATE VIEW inserimento_dati_vista AS
SELECT row_number() OVER ()::integer AS gid,
evento_investimento.ora,
evento_investimento.data,
rilevatore.nome_cognome,
specie.nome_comune,
specie.nome_scientifico
FROM evento_investimento
JOIN specie ON evento_investimento.specie_id = specie.specie_id
JOIN rilevatore ON evento_investimento.rilevatore_id = rilevatore.rilevatore_id;
CREATE OR REPLACE FUNCTION inserimento_dati_fun_2() RETURNS trigger AS $$
BEGIN
if not exists(select * from rilevatore where rilevatore.nome_cognome=new.nome_cognome) then
INSERT INTO rilevatore (nome_cognome)
VALUES (NEW.nome_cognome);
end if;
if not exists(select * from specie where specie.nome_comune=new.nome_comune) then
INSERT INTO specie (nome_comune, nome_scientifico)
VALUES (NEW.nome_comune, NEW.nome_scientifico);
end if;
INSERT INTO evento_investimento (data, ora, rilevatore_id, specie_id)
VALUES (NEW.data,NEW.ora,
(SELECT rilevatore_id FROM rilevatore WHERE rilevatore.nome_cognome = NEW.nome_cognome),
(SELECT specie_id FROM specie WHERE specie.nome_comune = NEW.nome_comune));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
create trigger inserimento_dati_fun_trg
instead of insert on inserimento_dati_vista for each row EXECUTE procedure inserimento_dati_fun_2();
Now, I want to add a function that allow to update all the tables by using the view inserimento_dati_vista.
I've tried with a simple code to update only the data column
CREATE OR REPLACE FUNCTION update_dati_fun_2() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'UPDATE') THEN
IF old.data is distinct from new.data then
UPDATE evento_investimento
SET data = new.data;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
create trigger update_dati_fun_2_trg
instead of update on inserimento_dati_vista for each row EXECUTE procedure update_dati_fun_2();
However when I perfomr the query in order to update only a row, the trigger update all the rows in the table. Here some code to fill data.
INSERT INTO inserimento_dati_vista
(data, ora, nome_cognome, nome_comune, nome_scientifico)
VALUES
('2020-01-01', '16:54:00','mario', 'lupo', 'Canis lupus'),
('2020-01-02', '13:54:00','luca', 'lontra', 'Lutra lutra');
UPDATE inserimento_dati_vista
SET data = '2021-01-02' where nome_cognome = 'luca'
Update function is:
CREATE OR REPLACE FUNCTION update_dati_fun_2() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'UPDATE') THEN
IF old.data is distinct from new.data then
UPDATE evento_investimento e
SET data = new.data
FROM rilevatore r
WHERE nome_cognome = new.nome_cognome AND r.rilevatore_id = e.rilevatore_id;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

PostgreSQL - Before Update Trigger determine which columns were updated

I have a table with many columns.
Need to find which columns value have changed.
I am using the following code. I am having a performance problem.
Can I do this with a different method?
CREATE OR REPLACE FUNCTION public.mytable()
RETURNS trigger AS $$
DECLARE
_rec record;
BEGIN
FOR _rec IN
SELECT
o.key
, o.value AS old_value
, n.value AS new_value
FROM json_each(to_json(new)) n
INNER JOIN json_each(to_json(old)) o ON o.key = n.key
LOOP
IF (_rec.old_value::text IS DISTINCT FROM _rec.new_value::text) THEN
/*
field name: _rec.key,
old values: _rec.old_value,
new value : _rec.new_value
*/
END IF;
END LOOP;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';

Postgresql trigger operating on both NEW and OLD records

This is primarily a style question.
I have an AFTER INSERT OR UPDATE trigger. I want to run the same query against whichever of the the NEW/OLD records are available. Rather than have conditionals checking TG_OP and duplicating the query, what is the cleanest way to determine which are available and to loop over them?
Example code:
CREATE FUNCTION myfunc() RETURNS TRIGGER AS $$
DECLARE
id int;
ids int[];
BEGIN
IF TG_OP IN ('INSERT', 'UPDATE') THEN
ids := ids || NEW.id;
END IF;
IF TG_OP IN ('UPDATE', 'DELETE') THEN
ids := ids || OLD.id;
END IF;
FOREACH id IN ARRAY ARRAY(SELECT DISTINCT UNNEST(ids))
LOOP
RAISE NOTICE 'myfunc called for % on id %', TG_OP, id;
/* RUN QUERY REFERENCING id */
END LOOP;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Is there a shorter/simpler/more idiomatic way to achieve that?
Array handling and a separate SELECT DISTINCT seem too expensive for the job. This should be cheaper:
CREATE FUNCTION myfunc()
RETURNS TRIGGER AS
$func$
DECLARE
_id int;
BEGIN
CASE TG_OP
WHEN 'INSERT', 'UPDATE' THEN
_id := NEW.id;
WHEN 'DELETE' THEN
_id := OLD.id;
END CASE;
FOR i IN 1..2 -- max 2 iterations
LOOP
RAISE NOTICE 'myfunc called for % on id %', TG_OP, _id;
/* RUN QUERY REFERENCING _id */
EXIT WHEN TG_OP <> 'UPDATE' OR i = 2; -- only continue in 1st round for UPDATE
EXIT WHEN _id = OLD.id; -- only continue for different value
_id := OLD.id;
END LOOP;
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
Related:
BREAK statement in PL/pgSQL
But I would probably just write a separate trigger function & trigger for each DML statement. Rather three very simple and faster functions than one generic but complex and slower one.

PostgreSQL update trigger Comparing Hstore values

I am creating trigger in PostgresSQL. On update I would like to compare all of the values in a Hstore column and update changes in my mirror table. I managed to get names of my columns in variable k but I am not able to get values using it from NEW and OLD.
CREATE OR REPLACE FUNCTION function_replication() RETURNS TRIGGER AS
$BODY$
DECLARE
k text;
BEGIN
FOR k IN SELECT key FROM EACH(hstore(NEW)) LOOP
IF NEW.k != OLD.k THEN
EXECUTE 'UPDATE ' || TG_TABLE_NAME || '_2' || 'SET ' || k || '=' || new.k || ' WHERE ID=$1.ID;' USING OLD;
END IF;
END LOOP;
RETURN NEW;
END;
$BODY$
language plpgsql;
You should operate on hstore representations of the records new and old. Also, use the format() function for better control and readibility.
create or replace function function_replication()
returns trigger as
$body$
declare
newh hstore = hstore(new);
oldh hstore = hstore(old);
key text;
begin
foreach key in array akeys(newh) loop
if newh->key != oldh->key then
execute format(
'update %s_2 set %s = %L where id = %s',
tg_table_name, key, newh->key, oldh->'id');
end if;
end loop;
return new;
end;
$body$
language plpgsql;
Another version - with minimalistic numbers of updates - in partially functional design (where it is possible).
This trigger should be AFTER trigger, to be ensured correct behave.
CREATE OR REPLACE FUNCTION function_replication()
RETURNS trigger AS $$
DECLARE
newh hstore;
oldh hstore;
update_vec text[];
pair text[];
BEGIN
IF new IS DISTINCT FROM old THEN
IF new.id <> old.id THEN
RAISE EXCEPTION 'id should be immutable';
END IF;
newh := hstore(new); oldh := hstore(old); update_vec := '{}';
FOREACH pair SLICE 1 IN ARRAY hstore_to_matrix(newh - oldh)
LOOP
update_vec := update_vec || format('%I = %L', pair[1], pair[2]);
END LOOP;
EXECUTE
format('UPDATE %I SET %s WHERE id = $1',
tg_table_name || '_2',
array_to_string(update_vec, ', '))
USING old.id;
END IF;
RETURN NEW; -- the value is not important in AFTER trg
END;
$$ LANGUAGE plpgsql;
CREATE TABLE foo(id int PRIMARY KEY, a int, b int);
CREATE TABLE foo_2(LIKE foo INCLUDING ALL);
CREATE TRIGGER xxx AFTER UPDATE ON foo
FOR EACH ROW EXECUTE PROCEDURE function_replication();
INSERT INTO foo VALUES(1, NULL, NULL);
INSERT INTO foo VALUES(2, 1,1);
INSERT INTO foo_2 VALUES(1, NULL, NULL);
INSERT INTO foo_2 VALUES(2, 1,1);
UPDATE foo SET a = 20, b = 30 WHERE id = 1;
UPDATE foo SET a = NULL WHERE id = 1;
This code is little bit more complex, but all what should be escaped is escaped and reduce number of executed UPDATE commands. UPDATE is full SQL command and the overhead of full SQL commands should be significantly higher than code that reduce number of full SQL commands.

How to create a trigger that checks values?

I need help with my trigger. For example i have Query like this
INSERT INTO test(id_users,id_type,id_color) VALUES(1,3,4);
in my data base i have table with name: test
ghost, id, id_users, id_type, id_color
and i need before insert or update to check
Create trigger
Begin
select id from test where ghost = false and id_users = 26 and id_type = 3
if NO execute INSERT
if YEST exit with no action
END
How can i creat this trigger ?
There are two ways, depending on how you want to manage the problem.
If you wish to silence it, use a before trigger and return null:
create function ignore_invalid_row() returns trigger as $$
begin
if not exists(
select 1
from test
where not ghost
and id_users = new.id_users
and id_type = new.id_type
)
then
return null;
end if;
return new;
end;
$$ language plpgsql;
create trigger ignore_invalid_row_tg before insert on test
for each row execute procedure ignore_invalid_row();
If you wish to raise it, use a constraint trigger and raise an exception:
create function reject_invalid_row() returns trigger as $$
begin
if not exists(
select 1
from test
where not ghost
and id_users = new.id_users
and id_type = new.id_type
)
then
raise exception 'message';
end if;
return null;
end;
$$ language plpgsql;
create constraint trigger reject_invalid_row_tg after insert on test
for each row execute procedure reject_invalid_row();
http://www.postgresql.org/docs/current/static/sql-createtrigger.html
First, you need to create a trigger function and then the trigger, based on it:
CREATE OR REPLACE FUNCTION my_trigger_function() RETURNS TRIGGER AS $BODY$ BEGIN
IF EXISTS (SELECT id FROM test WHERE ghost = false AND id_users = 26 AND id_type = 3) THEN
return NEW;
ELSE
return NULL;
END IF; END; $BODY$ LANGUAGE 'plpgsql';
Then, you create trigger based on this function:
CREATE TRIGGER t_my_trigger BEFORE INSERT ON test FOR EACH ROW EXECUTE PROCEDURE my_trigger_function();
For more on triggers, see postgres docs.