Is it possible to dynamically loop through a table's columns? - postgresql

I have a trigger function for a table test which has the following code snippet:
IF TG_OP='UPDATE' THEN
IF OLD.locked > 0 AND
( OLD.org_id <> NEW.org_id OR
OLD.document_code <> NEW.document_code OR
-- other columns ...
)
THEN
RAISE EXCEPTION 'Message';
-- more code
So I am statically checking all the column's new value with its previous value to ensure integrity. Now every time my business logic changes and I have to add new columns into that table, I will have to modify this trigger each time. I thought it would be better if somehow I could dynamically check all the columns of that table, without explicitly typing their name.
How can it be done?

From 9.0 beta2 documentation about WHEN clause in triggers, which might be able to be used in earlier versions within the trigger body:
OLD.* IS DISTINCT FROM NEW.*
or possibly (from 8.2 release notes)
IF row(new.*) IS DISTINCT FROM row(old.*)

Take a look at the information_schema, there is a view "columns". Execute a query to get all current columnnames from the table that fired the trigger:
SELECT
column_name
FROM
information_schema.columns
WHERE
table_schema = TG_TABLE_SCHEMA
AND
table_name = TG_TABLE_NAME;
Loop through the result and there you go!
More information can be found in the fine manual.

In Postgres 9.0 or later add a WHEN clause to your trigger definition (CREATE TRIGGER statement):
CREATE TRIGGER foo
BEFORE UPDATE
FOR EACH ROW
WHEN (OLD IS DISTINCT FROM NEW) -- parentheses required!
EXECUTE PROCEDURE ...;
Only possible for triggers BEFORE / AFTER UPDATE, where both OLD and NEW are defined. You'd get an exception trying to use this WHEN clause with INSERT or DELETE triggers.
And radically simplify the trigger function accordingly:
...
IF OLD.locked > 0 THEN
RAISE EXCEPTION 'Message';
END IF;
...
No need to test IF TG_OP='UPDATE' ... since this trigger only works for UPDATE anyway.
Or move that condition in the WHEN clause, too:
CREATE TRIGGER foo
BEFORE UPDATE
FOR EACH ROW
WHEN (OLD.locked > 0
AND OLD IS DISTINCT FROM NEW)
EXECUTE PROCEDURE ...;
Leaving only an unconditional RAISE EXCEPTION in your trigger function, which is only called when needed to begin with.
Read the fine print:
In a BEFORE trigger, the WHEN condition is evaluated just before the
function is or would be executed, so using WHEN is not materially
different from testing the same condition at the beginning of the
trigger function. Note in particular that the NEW row seen by the
condition is the current value, as possibly modified by earlier
triggers. Also, a BEFORE trigger's WHEN condition is not allowed to
examine the system columns of the NEW row (such as oid), because those
won't have been set yet.
In an AFTER trigger, the WHEN condition is evaluated just after the
row update occurs, and it determines whether an event is queued to
fire the trigger at the end of statement. So when an AFTER trigger's
WHEN condition does not return true, it is not necessary to queue an
event nor to re-fetch the row at end of statement. This can result in
significant speedups in statements that modify many rows, if the
trigger only needs to be fired for a few of the rows.
Related:
Fire trigger on update of columnA or ColumnB or ColumnC
To also address the question title
Is it possible to dynamically loop through a table's columns?
Yes. Examples:
Handle result when dynamic SQL is in a loop
Removing all columns with given name

Iteration over RECORD variable inside trigger

Use pl/perl or pl/python. They are much better suited for such tasks. much better.
You can also install hstore-new, and use it's row->hstore semantics, but that's definitely not a good idea when using normal datatypes.

Related

Is INSTEAD OF UPDATE trigger the best option

I have to check when a table is inserted to/updated to see if a column value exists for the same HotelID and different RoomNo in the same table. I'm thinking that an INSTEAD OF trigger on the table would be a good option, but I read that it's a bad idea to update/insert the table the trigger executes on inside the trigger and you should create the trigger on a view instead (which raises more questions for me)
Is it ok to create a trigger like this? Is there a better option?
CREATE TRIGGER dbo.tgr_tblInterfaceRoomMappingUpsert
ON dbo.tblInterfaceRoomMapping
INSTEAD OF INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #txtRoomNo nvarchar(20)
SELECT #txtRoomNo = Sonifi_RoomNo
FROM dbo.tblInterfaceRoomMapping r
INNER JOIN INSERTED i
ON r.iHotelID = i.iHotelID
AND r.Sonifi_RoomNo = i.Sonifi_RoomNo
AND r.txtRoomNo <> i.txtRoomNo
IF #txtRoomNo IS NULL
BEGIN
-- Insert/update the record
END
ELSE
BEGIN
-- Raise error
END
END
GO
So it sounds like you only want 1 row per combo of HotelID and Sonifi_RoomNo.
CREATE UNIQUE INDEX UQ_dbo_tblInterfaceRoomMapping
ON dbo.tblInterfaceRoomMapping(HotelID,Sonifi_RoomNo)
Now if you try and put a second row with the same values, it will bark at you.
It's (usually) not okay to create a trigger like that.
Your trigger assumes a single row update or insert will only ever occur - is that guaranteed?
What will be the value of #txtRoomNo if multiple rows are inserted or updated in the same batch?
Eg, if an update is performed against the table resulting in 1 row with correct data and 1 row with incorrect data, how do you think your trigger would cope in that situation? Remember triggers fire once per insert/update, not per row.
Depending on your requirments you could keep the instead of trigger concept, however I would suggest a separate trigger for inserts and for updates.
In each you can then insert / update and include a where not exists clause to only allow valid inserts / updates, ignoring inserting or updating anything invalid.
I would avoid raising an error in the trigger, if you need to handle bad data you could also insert into some logging table with the reverse where exists logic and then handle separately.
Ultimately though, it would be best for the application to check if the roomNo is already used.

How to invoke a trigger on all rows manually in postgres

My trigger is defined the following way:
CREATE TRIGGER update_contract_finished_at
AFTER INSERT OR DELETE OR UPDATE OF performed_on
ON task
FOR EACH ROW
EXECUTE PROCEDURE update_contract_finished_at_function();
I now want to evoke this trigger to set the variables which are updated by the trigger. How do I do that?
Something like
for each row in task
execute procedure update_contract_finished_at_function();
I know I can update with a standard update set statement. I also want to verifiy that my trigger works on all the data correctly.
I'd write a slightly modified copy of update_contract_finished_at_function that takes type task as input and returns void.
Then replace NEW in the trigger function with $1 and call the function like this:
SELECT copy_func(task) FROM task;
If the functions are almost identical, it should be good enough to test the trigget function.
The way to manually trigger your on update trigger once would be:
UPDATE task SET performed_on = performed_on
however depending on how complicated your logic is in there and how many rows you have in the table a separate query might be significantly faster for initializing a large number of rows.
Since you mentioned you want to test the behaviour of your trigger you can clone the table or do a table or database dump and restore the data afterwards. If this is a live system you should instead do a database dump, restore to another system, add your trigger, test it, repeat from restore until you nail it... and only after you're sure it does what you want update the live system with it.
I ended up writing a PL/pgSQL function that in a loop processes all events in chronological order and calling it:
create or replace function process_event_history()
returns void
language plpgsql
as
$$
declare
event record;
begin
for event in
select id, timestamp
from events
order by timestamp
loop
update events set timestamp = event.timestamp
where id = event.id;
end loop;
end;
$$;
--;;
-- Execute the above function causing the trigger to run for all events.
select process_event_history();
--;;
-- Remove the temporary processing function.
drop function process_event_history();

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

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;
`)
}

Trigger with multiple WHEN conditions

How do I include the columns I need to monitor? I.e. instead of one WHEN condition I want to have 3 WHEN conditions:
CREATE TRIGGER freeradius.insert_into_day_summations
BEFORE INSERT ON freeradius.day_guiding_usage
FOR EACH ROW
WHEN (OLD.col1 IS DISTINCT FROM NEW.col1)
WHEN (OLD.col2 IS DISTINCT FROM NEW.col2)
WHEN (OLD.col3 IS DISTINCT FROM NEW.col3)
EXECUTE procedure update_sessioninfo();
Form a single expression with OR or AND - depending on whether you want to trigger when all conditions are met or when either one condition is met:
CREATE TRIGGER update_day_summations -- see below
BEFORE UPDATE ON freeradius.day_guiding_usage
FOR EACH ROW
WHEN (OLD.col1 IS DISTINCT FROM NEW.col1
OR OLD.col2 IS DISTINCT FROM NEW.col2
OR OLD.col3 IS DISTINCT FROM NEW.col3)
EXECUTE procedure update_sessioninfo();
It's just a boolean expression, can involve all columns of the row.
However, your expressions only make sense for UPDATE, not for INSERT. There is no OLD record for inserts. The manual on CREATE TRIGGER:
condition
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.
And the trigger name itself cannot be schema-qualified. Quoting the manual once more:
name
The name to give the new trigger. This must be distinct from the name of any other trigger for the same table. The
name cannot be schema-qualified
Bold emphasis mine.
TG_WHEN
Data type text; a string of BEFORE, AFTER, or INSTEAD OF, depending on the trigger's definition.
here is a link may use
http://www.postgresql.org/docs/9.2/static/plpgsql-trigger.html