© Laurenz Albe 2021
In this article, I’ll talk about row level triggers, which are the most frequently used kind of triggers. I will describe what the return value of the trigger function means and suggest a useful code simplification.
Triggers in PostgreSQL
A trigger in PostgreSQL consists of two parts:
- a trigger function
- the actual trigger, which invokes the trigger function
This architecture has the advantage that trigger functions can be reused: triggers on different tables can use the same trigger function.
PostgreSQL has event triggers and triggers that get activated by data modifications. We will only consider the latter in this article.
There are statement level triggers (
FOR EACH STATEMENT), but we will focus on the more common row level triggers (
FOR EACH ROW). For such triggers, the trigger function runs once per modified table row.
Furthermore, triggers can run
INSTEAD OF the data modification that triggers it.
INSTEAD OF triggers must be row level triggers on views.
The return value of a trigger function
Trigger functions are always declared as “
RETURNS trigger”, but what you actually have to return is
- for statement level triggers, the value NULL
- for row level triggers, a row of the table on which the trigger is defined
The return value is ignored for row level
AFTER triggers, so you may as well return NULL in that case. That leaves row level
BEFORE triggers as the only interesting case.
In row level
BEFORE triggers, the return value has the following meaning:
- if the trigger returns NULL, the triggering operation is aborted, and the row will not be modified
UPDATEtriggers, the returned row is the input for the triggering DML statement
Note also that you can have more than one row level
BEFORE trigger on a table. In this case, the triggers are executed in the alphabetical order of their name, and the result of the previous trigger function becomes the input for the next trigger function.
OLD in row level triggers
The special variables
OLD in a row level trigger function contain the new and the old row version. They can be modified and used in the
NEW is NULL in
ON DELETE triggers and
OLD is NULL in
ON INSERT triggers.
Example: an auditing row level trigger
We want to capture data modifications to a table
CREATE TABLE mytab ( id bigint GENERATED ALWAYS AS IDENTITY NOT NULL, val text ); CREATE TABLE mytab_hist ( mod_time timestamp with time zone DEFAULT clock_timestamp() NOT NULL, operation text NOT NULL, id bigint NOT NULL, val text, PRIMARY KEY (id, mod_time) );
Here we assume that
id will never change; otherwise we would have to come up with something more complicated.
The trigger definition will look like
CREATE TRIGGER mytab_hist BEFORE INSERT OR UPDATE OR DELETE ON mytab FOR EACH ROW EXECUTE FUNCTION mytab_hist();
A possible trigger function could look like:
CREATE FUNCTION mytab_hist() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN IF TG_OP = 'DELETE' THEN INSERT INTO mytab_hist (operation, id, val) VALUES (TG_OP, OLD.id, NULL); RETURN OLD; ELSE INSERT INTO mytab_hist (operation, id, val) VALUES (TG_OP, NEW.id, NEW.val); RETURN NEW; END IF; END;$$;
TG_OP contains the triggering event (
Simplification of the row level trigger function
Note how similar the code for both branches of the
IF statement is. It would be nice to simplify that. Indeed the
coalesce function makes it possible to write the same function in a much simpler fashion:
CREATE OR REPLACE FUNCTION mytab_hist() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN INSERT INTO mytab_hist (operation, id, val) VALUES (TG_OP, coalesce(NEW.id, OLD.id), NEW.val); RETURN coalesce(NEW, OLD); END;$$;
coalesce will return the first of its arguments that is not NULL. This does exactly the right thing in our case, because
NEW is NULL in
ON DELETE triggers.
Instead of writing complicated conditional code for the return value of a row level trigger, resort to the simple
RETURN coalesce(NEW, OLD);
which is almost always the right thing.
If you want to know more about triggers, you may want to read my article about constraint triggers.