Postgresql trigger operating on both NEW and OLD records - postgresql

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.

Related

PostgreSQL subquery with IF EXISTS in trigger function

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;

PostgreSQL 10 error relation new doesn't exist on before update trigger

My before update trigger throws an error on updating single row in table company.
ERROR: relation "new" does not exist, row 7.
Trigger and trigger function code:
CREATE OR REPLACE FUNCTION public.check_company_transitivity()
RETURNS trigger
AS $$
DECLARE
cmp uuid;
head_company uuid;
audited_company uuid;
BEGIN
audited_company := (select NEW.id from NEW);
cmp:=audited_company;
head_company:='*';
while head_company is not null loop
head_company:=(select head_company from company where id=cmp);
if audited_company=head_company then
raise exception 'transitive closure';
else
cmp=head_company;
end if;
end loop;
return new;
END;
$$ LANGUAGE plpgsql;
create trigger check_company_transitivity
before update
on
public.company for each row execute procedure check_company_transitivity();
I can’t understand what the problem is, I would be appreciate for any help.
You can use the values from NEW directly
audited_company := NEW.id;

How to pass NEW.* to EXECUTE in trigger function

I have a simple mission is inserting huge MD5 values into tables (partitioned table), and have created a trigger and also a trigger function to instead of INSERT operation. And in function I checked the first two characters of NEW.md5 to determine which table should be inserted.
DECLARE
tb text;
BEGIN
IF TG_OP = 'INSERT' THEN
tb = 'samples_' || left(NEW.md5, 2);
EXECUTE(format('INSERT INTO %s VALUES (%s);', tb, NEW.*)); <- WRONG
END IF;
RETURN NULL;
END;
The question is how to concat the NEW.* into the SQL statement?
Best with the USING clause of EXECUTE:
CREATE FUNCTION foo ()
RETURNS trigger AS
$func$
BEGIN
IF TG_OP = 'INSERT' THEN
EXECUTE format('INSERT INTO %s SELECT $1.*'
, 'samples_' || left(NEW.md5, 2);
USING NEW;
END IF;
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
And EXECUTE does not require parentheses.
And you are aware that identifiers are folded to lower case unless quoted where necessary (%I instead of %s in format()).
More details:
INSERT with dynamic table name in trigger function
How to dynamically use TG_TABLE_NAME in PostgreSQL 8.2?

How to keep looping even error happend?

I wrote a PL/pgsql to batch create index on tables
CREATE OR REPLACE FUNCTION create_index() RETURNS void AS
$BODY$
DECLARE
r INTEGER;
BEGIN
FOR r IN 1..1000
LOOP
EXECUTE format(
' CREATE INDEX idx_abc_id_' || r::text ||
' ON abc_id_' || r::text ||
' USING btree
(key);');
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;
it has one problem, if partition abc_500 doesn't exist, then the how create index function will fail and do nothing.
How to make loop keep going through even if create_index made an error on one of the table in between?
I think a better approach would be to not hardcode the number for the loop, but iterate over the existing tables:
CREATE OR REPLACE FUNCTION create_index() RETURNS void AS
$BODY$
DECLARE
r record;
BEGIN
FOR r IN select tablename, regexp_replace(tablename, '[^0-9]+','') as idx_nr
from pg_tables
where tablename ~ 'abc_id_[0-9]+'
LOOP
EXECUTE format('CREATE INDEX %I ON %I USING btree (key)',
'idx_abc_id_'||r.idx_nr,
r.tablename);
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;
When you use the format() function is better to use the proper place holders for identifiers.
If you also want to ignore any error when creating the index on an existing table, you need to catch the exception and ignore it:
CREATE OR REPLACE FUNCTION create_index() RETURNS void AS
$BODY$
DECLARE
r record;
msg text;
BEGIN
FOR r IN select tablename, regexp_replace(tablename, '[^0-9]+','') as idx_nr
from pg_tables
where tablename ~ 'abc_id_[0-9]+'
LOOP
BEGIN
EXECUTE format('CREATE INDEX %I ON %I USING btree (key)',
'idx_abc_id_'||r.idx_nr,
r.tablename);
EXCEPTION
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS msg = MESSAGE_TEXT;
RAISE NOTICE 'Could not create index for: %, %', r.idx_nr, msg;
END;
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;

Endless loop in trigger function

This is a trigger that is called by either an insert, update or a delete on a table. It is guaranteed the calling table has all the columns impacted and a deletes table also exists.
CREATE OR REPLACE FUNCTION sample_trigger_func() RETURNS TRIGGER AS $$
DECLARE
operation_code char;
table_name varchar(50);
delete_table_name varchar(50);
old_id integer;
BEGIN
table_name = TG_TABLE_NAME;
delete_table_name = TG_TABLE_NAME || '_deletes';
SELECT SUBSTR(TG_OP, 1, 1)::CHAR INTO operation_code;
IF TG_OP = 'DELETE' THEN
OLD.mod_op = operation_code;
OLD.mod_date = now();
RAISE INFO 'OLD: %', (OLD).name;
EXECUTE format('INSERT INTO %s VALUES %s', delete_table_name, (OLD).*);
ELSE
EXECUTE format('UPDATE TABLE %s SET mod_op = %s AND mod_date = %s'
, TG_TABLE_NAME, operation_code, now());
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
The ELSE branch triggers an endless loop. There may be more problems.
How to fix it?
The ELSE branch can be radically simplified. But a couple more things are inefficient / inaccurate / dangerous:
CREATE OR REPLACE FUNCTION sample_trigger_func()
RETURNS TRIGGER AS
$func$
BEGIN
IF TG_OP = 'DELETE' THEN
RAISE INFO 'OLD: %', OLD.name;
EXECUTE format('INSERT INTO %I SELECT ($1).*', TG_TABLE_NAME || '_deletes')
USING OLD #= hstore('{mod_op, mod_datetime}'::text[]
, ARRAY[left(TG_OP, 1), now()::text]);
RETURN OLD;
ELSE -- insert, update
NEW.mod_op := left(TG_OP, 1);
NEW.mod_datetime := now();
RETURN NEW;
END IF;
END
$func$ LANGUAGE plpgsql;
In the ELSE branch just assign to NEW directly. No need for more dynamic SQL - which would fire the same trigger again causing an endless loop. That's the primary error.
RETURN NEW; outside the IF construct would break your trigger function for DELETE, since NEW is not assigned for DELETEs.
A key feature is the use of hstore and the hstore operator #= to dynamically change two selected fields of the well-known row type - that is unknown at the time of writing the code. This way you do not tamper with the original OLD value, which might have surprising side effect if you have more triggers down the chain of events.
OLD #= hstore('{mod_op, mod_datetime}'::text[]
, ARRAY[left(TG_OP, 1), now()::text]);
The additional module hstore must be installed. Details:
How to set value of composite variable field using dynamic SQL
Passing column names dynamically for a record variable in PostgreSQL
Using the hstore(text[], text[]) variant here to construct an hstore value with multiple fields on the fly.
The assignment operator in plpgsql is :=:
The forgotten assignment operator "=" and the commonplace ":="
Note that I used the column name mod_datetime instead of the misleading mod_date, since the column is obviously a timestamp and not a date.
I added a couple of other improvements while being at it. And the trigger itself should look like this:
CREATE TRIGGER insupdel_bef
BEFORE INSERT OR UPDATE OR DELETE ON table_name
FOR EACH ROW EXECUTE PROCEDURE sample_trigger_func();
SQL Fiddle.