Update field updated by trigger - postgresql

I have a table with multiple fields and an additional outofsync field.
Created a trigger and trigger function to set outofsync field value to true before any update/insert.
Trigger:
CREATE TRIGGER goods_update_outofsync
BEFORE UPDATE
ON goods
FOR EACH ROW
EXECUTE PROCEDURE tg_update_goods_outofsync();
Trigger function:
CREATE OR REPLACE FUNCTION tg_update_goods_outofsync()
RETURNS trigger AS
$BODY$
BEGIN
NEW.outofsync=true;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION tg_update_goods_outofsync()
OWNER TO postgres;
And now comes to a "simple" question I am not able to find answer: how to update manually outofsync field to false, because after each attempt it is automatically changed to true by trigger.
EDIT:
This almost works:
IF (NEW.outofsync = OLD.outofsync) THEN
NEW.outofsync=true;
END IF;
Except when value of outofsync field is already false and I want to set it to false, because it became true then...
Thank you for your help in advance!

At least four options:
Set to in sync as another user and test current_user in the trigger;
Define a custom config variable (GUC) and SET LOCAL or set_config(...) it in the transaction before updating the in sync field; test that GUC in the trigger and change behaviour based on it;
Temporarily disable the trigger in the transaction before setting in sync;
Have the trigger check if all the other values are unchanged by the update and allow in sync to be set to true if no other values have changed. Use IS DISTINCT FROM for this test to handle nulls conveniently.
I'd probably use a custom GUC myself, with current_setting('my.guc') to fetch the value from within the trigger.
If you're on Pg 9.1 or older you must add my (or whatever you really call the prefix) to custom_variable_classes. In 9.2 and above any variable with a period (.) in it is assumed to be a custom variable.
See also passing user ID to triggers.

Related

Creating a trigger function in SQL

I've been given a task to create a trigger function for this question:
Add an update to the support ticket from a given staff member. If the ticket is closed no more updates should be allowed by either customers or staff members.
Insert an update to a ticket with values: ticketUpdateID = 10050; Message ='Highlight folder for backup and press to trash or right click and choose delete'; TicketID = 1010 ; StaffID=2
All I've managed so far is:
CREATE OR REPLACE FUNCTION update_ticket()
RETURNS trigger AS $BODY$
BEGIN
I would just like to know the layout of what i would have to put next.
Thanks
The question becomes "How do you want to handle bypassing the update: silently ignore the action or raise an exception"? Postgres provides for both cases. The trigger function for an Update DML receives 2 pseudo rows conveniently named old and new (at least by default) which represent the current database row and the "tobe" database row respectively. Somewhere in the table definition you have some column indicating the current status of the some value indicating "closed". Unfortunately you have not defined either so I will just make something up for demo purpose. The following will suppress the any update on a 'closed' ticket. I will leave Raising an exception for your research.
create or replace
function update_ticket()
returns trigger
language plpgsql
as $$
begin
if old.status = 'closed' then
return null;
end if;
return new;
end;
$$;

How to migrate a Firebird's Trigger to PostgreSQL

I'm migrating an entire database from Firebird to PostgreSQL and it's not rocket science. But I'm having serious trouble with triggers. Specially the Firebird's POSITION argument.
Actually, I'm searching about the POSITION behavior. I need it but in PostgreSQL.
Those are the Triggers in Firebird:
This Trigger needs to be executed first:
/* Trigger: TRG_CFE_ESTOQUE_PROCESSADO */
CREATE OR ALTER TRIGGER TRG_CFE_ESTOQUE_PROCESSADO FOR ITENS_CFE
BEFORE UPDATE POSITION 0
AS
BEGIN
IF(NEW.ITE_QTD <> OLD.ITE_QTD)THEN
BEGIN
NEW.ITE_ESTOQUE_PROCESSADO = 'N';
END
END
And this one needs to be executed after:
/* Trigger: TRG_CFE_ESTOQUE_EXCLUIDO */
CREATE OR ALTER TRIGGER TRG_CFE_ESTOQUE_EXCLUIDO FOR ITENS_CFE
BEFORE DELETE POSITION 1
AS
BEGIN
UPDATE ITENS_CFE
SET ITE_ESTOQUE_PROCESSADO = 'N'
WHERE PRO_CODIGO = OLD.PRO_CODIGO
AND CFE_CODIGO = OLD.CFE_CODIGO;
END
For now, I'm not testing it, just searching for a way to reproduce the expected behavior.
Searching again, I've found something in the PostgreSQL Documentation:
If multiple triggers of the same kind are defined for the same event, they will be fired in alphabetical order by name
And I think it will do the magic.
But is this the best way of doing it?
The standard way I've defined trigger would be like the following:
CREATE OR REPLACE FUNCTION func_table_x_after_insert()
RETURNS TRIGGER
AS $$
BEGIN
INSERT INTO table_y
(id)
VALUES
(NEW.id)
;
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
CREATE TRIGGER trig_table_x_after_insert
AFTER INSERT ON table_x
FOR EACH ROW EXECUTE PROCEDURE func_table_x_after_insert();
The function you define can handle multiple steps.

How to freeze field-values in postgres?

I would like to make sure that the values of certain required fields can not be changed later on. Is there a way to define this on the schema level?
Currently, I'm thinking about implementing this using a Record Trigger to raise an exception if a value change is noticed but this feels clunky.
E.g.:
BEGIN
IF (TG_OP = 'UPDATE') THEN
IF (NEW.product_id !== OLD.product_id) THEN
RAISE EXCEPTION 'Attempt to change frozen field "product_id" on UPDATE.'
END IF;
END IF;
END
If you want a trigger with comparison on a field, you can save execution by specifying condition on the trigger itself:
A Boolean expression that determines whether the trigger function will
actually be executed. If WHEN is specified, the function will only be
called if the condition returns true. In FOR EACH ROW triggers, the
WHEN condition can refer to columns of the old and/or new row values
by writing OLD.column_name or NEW.column_name respectively. Of course,
INSERT triggers cannot refer to OLD and DELETE triggers cannot refer
to NEW.
eg:
CREATE TRIGGER check_update
BEFORE UPDATE ON accounts
FOR EACH ROW
WHEN (OLD.product_id IS DISTINCT FROM NEW.product_id)
EXECUTE PROCEDURE check_account_update();
Of course it does not freeze anything. Now you cant change it with update, unless you disable the trigger, update and enable trigger back. But at least later requires alter table, not just update

Postgresql: run trigger AFTER update FOR EACH STATEMENT ONLY if data changed

In Postgresql I can have two kinds of triggers: FOR EACH ROW and FOR EACH STATEMENT. If I do a FOR EACH ROW trigger, I can add a WHERE clause something like OLD.* != NEW.* so it only fires if something has actually changed. Is there any way to do something similar with STATEMENT level triggers? I know I can't do the same thing since OLD and NEW aren't available, but I was thinking perhaps there might be a way to check the number of rows changed from within my function itself or the like.
Usage case: I am using the postgresql NOTIFY system to notify my app when data changes. Ideally, the app would get a single notification each time one or more records changes, and not get notified at all if data stays the same (even if an UPDATE was run). With a basic AFTER UPDATE FOR EACH STATEMENT trigger, I am getting notified every time an update statement runs - even if it doesn't actually change anything.
You should create two triggers: before update for each row and after update for each statement.
The first trigger checks if the table is being updated and sets a flag if so.
The second trigger checks the flag and performs notify if it was set.
You can use a custom configuration parameter as the flag (e.g. flags.the_table).
The solution is simple and safe, as the parameter is local in the current session.
create or replace function before_each_row_on_the_table()
returns trigger language plpgsql
as $$
begin
if new <> old then
set flags.the_table to 'on';
end if;
return new;
end $$;
create or replace function after_each_statement_on_the_table()
returns trigger language plpgsql
as $$
begin
if current_setting('flags.the_table', true) = 'on' then
notify your_channel, 'the_table was updated';
set flags.the_table to 'off';
end if;
return null;
end $$;
create trigger before_each_row_on_the_table
before update on the_table
for each row execute procedure before_each_row_on_the_table();
create trigger after_each_statement_on_the_table
after update on the_table
for each statement execute procedure after_each_statement_on_the_table();
The function current_setting() with two arguments is available in Postgres 9.6 or later.

Fire trigger on update of columnA or ColumnB or ColumnC

I have the code to fire a trigger only on an update of a single specific column. The trigger is used to fire a function that will raise a postgres "notify" event, which I am listening for and will need to test and validate the newly input details. There are many values on the account_details table which could be change which do not require an account validate, so a trigger on AFTER UPDATE only (without a when) is no good.
CREATE TRIGGER trigger_update_account_details
AFTER UPDATE ON account_details
FOR EACH ROW
WHEN (OLD.email IS DISTINCT FROM NEW.email)
EXECUTE PROCEDURE notify_insert_account_details();
But I want to fire the trigger if one of many columns change, something like
WHEN (OLD.email IS DISTINCT FROM NEW.email OR
OLD.username IS DISTINCT FROM NEW.username OR
OLD.password IS DISTINCT FROM NEW.password)
But OR is not a valid keyword for a trigger. Trying to search for the keyword to use instead of OR doesn't seem to bring up anything due the nature of the word OR :-(
The WHEN clause of the trigger definition expects a boolean expression and you can use OR operators in it. This just works (as long as all columns exist in the table account_details). I am using similar triggers myself:
CREATE TRIGGER trigger_update_account_details
AFTER UPDATE ON account_details
FOR EACH ROW
WHEN (OLD.email IS DISTINCT FROM NEW.email
OR OLD.username IS DISTINCT FROM NEW.username
OR OLD.password IS DISTINCT FROM NEW.password)
EXECUTE FUNCTION notify_insert_account_details();
In Postgres 10 or older use the (misleading) key word PROCEDURE instead of FUNCTION. See:
Trigger uses a procedure or a function?
Evaluating the expression has a tiny cost, but this is probably more reliable than the alternative:
CREATE TRIGGER ... AFTER UPDATE OF email, username, password ...
Because, quoting the manual:
A column-specific trigger (one defined using the UPDATE OFcolumn_name
syntax) will fire when any of its columns are listed as targets in the
UPDATE command's SET list. It is possible for a column's value to
change even when the trigger is not fired, because changes made to the
row's contents by BEFORE UPDATE triggers are not considered.
Conversely, a command such as UPDATE ... SET x = x ... will fire a
trigger on column x, even though the column's value did not change.
ROW type syntax is shorter to check on many columns (doing the same):
...
WHEN ((OLD.email, OLD.username, OLD.password, ...)
IS DISTINCT FROM
(NEW.email, NEW.username, NEW.password, ...))
...
Or, to check for every visible user column in the row:
...
WHEN (OLD IS DISTINCT FROM NEW)
...
I don't think you need the WHEN clause. You can specify the columns in question in the UPDATE clause:
CREATE TRIGGER trigger_update_account_details
AFTER UPDATE OF email, username, password ON account_details
FOR EACH ROW
EXECUTE PROCEDURE notify_insert_account_details();
The above solutions were not working for me properly. So after reading through documentation again. I found few things to take note of.
BEFORE UPDATE ON - AFTER UPDATE ON triggers are executed differently. Since my procedure was returning the NEW record with updated value. It was not working in AFTER trigger and in BEFORE trigger, the OR statements inside WHEN clause needed to be enclosed by braces.
CREATE TRIGGER check_update
BEFORE UPDATE ON some_table
FOR EACH ROW
WHEN ((OLD.colum_name_1 IS DISTINCT FROM NEW.colum_name_1) OR (OLD.colum_name_2 IS DISTINCT FROM NEW.colum_name_2))
EXECUTE PROCEDURE update_updated_at_column();
And the procedure
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
I'm in a bit of a rush but here's the solution I went with. I wanted to updated a column named "receivedAt" anytime the "answer" column changes (yes, my columns are camel case and my tables are capitalized... don't ask...). I also wanted it to null out if the answer was nulled (edge case that shouldn't ever really happen). But I didn't want this trigger to fire every time any row updates at all, as that could be costly.
I decided to combine the strategies used above, trusting in Postgres to do what it's supposed to in a performant way. I think some of them kind of reinvent the wheel and are inefficient in that they will fire any time any update is made.
I use knex migrations to manage my database, so I'll just go ahead and paste the whole thing in here.
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.raw(`
CREATE OR REPLACE FUNCTION question_update_received_at_when_answer_changes()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
NEW."receivedAt" = NOW();
IF NEW."answer" IS NULL THEN
NEW."receivedAt" = NULL;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trigger_question_answer_received_at ON "Question";
CREATE TRIGGER trigger_question_answer_received_at
BEFORE UPDATE OF "answer" ON "Question"
FOR EACH ROW
WHEN (OLD."answer" IS DISTINCT FROM NEW."answer")
EXECUTE PROCEDURE question_update_received_at_when_answer_changes();
`)
}
export async function down(knex: Knex): Promise<void> {
await knex.raw(`
DROP TRIGGER trigger_question_answer_received_at on "Question";
DROP FUNCTION question_update_received_at_when_answer_changes;
`)
}