select statement in postgres function called inside a trigger - postgresql

I'm trying to develop a notification system for the backend of a social media application/website. For now I'm focusing on status updates. What I'm going to do, is putting a trigger on the postgres table that related to status updates so that every time a new status update is posted, a notification is sent to my code.
So far I have been able to do that. But an extra feature that I like to implement is extracting all of the people who follow the user who posted the status update, so that I can also send them a notification that the person they're following has posted a new status update.
Of course it can be implemented by first receiving the notification for a new status update from postgres, extracting the user id of the person who posted it, and make a query to the database to find out which users follow them.
But I figured it would be more efficient if I don't make the query, and instead, each time postgres wants to send me a notification about a new status update, it also makes a query to find out which users are following the poster of the status update and send that information along with the notification for the new status update.
But I can't figure out how I can make a query in a postgres function that depends on the argument of that function, and then send the result of that query along with the argument as a notification.
this is what I've tried:
create table example (c1 text, c2 text);
create function notif()
returns trigger as
$$
begin
perform pg_notify('event',row_to_json(new)::text);
return new;
end;
$$ language plpgsql;
create trigger trig after insert
on example
for each row execute procedure notif();
And then I listen to the event channel from my code and receive the row that was inserted. But I want to do a select statement based on the new row in my notif() function and send the result with the new row to the listening code.
I'd appreciate any clarification
Thanks

Something like this?
CREATE FUNCTION notif()
RETURNS TRIGGER AS $$
DECLARE
data JSONB;
result JSONB;
BEGIN
SELECT json_agg(tmp) -- requires Postgres9.3+
INTO data
FROM (
-- your subquery goes here, for example:
SELECT followers.following_user_id
FROM followers
WHERE followers.followed_user_id = NEW.user_id
) tmp;
result := json_build_object('data', data, 'row', row_to_json(NEW));
PERFORM pg_notify('event', result::TEXT);
RETURN NEW;
END;
$$ language plpgsql;
Also from comments:
But somehow magically using return new the row is returned within the notification.
You misunderstand things. Return and notification are two different things.
First of all lets deal with return. For AFTER INSERT triggers the return value is totally ignored:
The return value of a row-level trigger fired AFTER or a statement-level trigger fired BEFORE or AFTER is always ignored; it might as well be null.
The return value only matters for BEFORE triggers. In which case you can modify (or even prevent) the row before writing to the table. See this: https://www.postgresql.org/docs/9.2/plpgsql-trigger.html This has nothing to do with notifications.
So what about notifications? Whatever you receive from a notification is what you pass as second argument to pg_notify. All of that is quite well documented: https://www.postgresql.org/docs/9.0/sql-notify.html

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 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();

A trigger fires on a table, but the select on the table returns null. How can I create the code to be able to access the row that fired the trigger?

A trigger fires on a table, but the select on the table returns null. How can I create the code to be able to access the row that fired the trigger?
I have the following in the trigger:
begin
dws_edi_api.init_edi_message(message_id,order_no',supplier_no');
end;
This fires on update of the column row_state in the table out_message_tab
The event fires OK but when in the procedure dws_edi_api.init_edi_message_line I do a select c08 from out_message_tab where message_id = message_id_ (variable from the trigger). it returns null.
I assume the change hasnt been committed. I have tried adding a commit as the first line in my code to force the change to commit but that doesnt help. I have tried adding a dbms_lock.sleep(!0) but that doesnt help either.
I add the code to the procedure in the "show some code box"
procedure init_edi_message_line(message_id in number) is
pragma autonomous_transaction;
message_id_ number;
order_no_ varchar2(20);
supplier_no_ varchar2(20);
c08_ varchar2(200);
cursor c1 is
select c08
from jdifs.out_message_line_tab
where message_id = message_id_
and name = 'HEADER';
begin
-- dbms_lock.sleep(10);
message_id_ := message_id;
open c1;
loop
fetch c1
into c08_;
exit when c08_ is not null;
insert into jdifs.jdws_temp_line_tab
values
(message_id_, '2', c08_, '4');
commit;
END LOOP;
close c1;
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- Do something
null;
WHEN OTHERS THEN
null;
end init_edi_message_line;
EDIT:
Hi, no this didnt solve the problem unfortunately,
I will try again to explain as thourougly as possible.
I have a trigger on the table called out_message_line_tab. When a row is created in that table it contains a big number of columns.
the ones that are interesting to me are message_id(which is a sequential number), order_no (P123456), supplier_no(11242), linenumber(1), part_no (F1524).
When the trigger fires data needs to be fetched from that table (and a table "connected to this table" in this case, out_message_tab.
So the trigger is on out_message_line_tab, but it isnt enough to send the values in the trigger to the procedure, since I need some data from the other table as well.
The primary key between the tables out_message_tab and out_message_line_tab is message_id
So my problem is how to do the select from out_message_tab where message_id = message_id(primary key from out_message_line_tab
When I do, it just says no data found. I assume its because it has not been commited yet.
I hope this is clearer.
Your procedure init_edi_message_line() is defined using pragma autonomous_transaction. That means it executes in a completely separate session. Consequently it cannot see any of the uncommitted data in the session which fired the trigger.
If you want init_edi_message_line() to process data from that session your triggers needs to pass everything to the procedure as an argument. However it's not clear exactly what you're doing - is out_message_line_tab the table which owns the trigger? - so I can't guarantee that it will be easy for you to make the necessary changes.

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.

How do I make a trigger to update a column in another table?

So I am working on adding a last updated time to the database for my app's server. The idea is that it will record the time an update is applied to one of our trips and then the app can send a get request to figure out if it's got all of the correct up to date information.
I've added the column to our table, and provided the service for it all, and finally manage to get a trigger going to update the column every time a change is made to a trip in it's trip table. My problem now comes from the fact that the information that pertains to a trip is stored across a multitude of other tables as well (for instance, there are tables for the routes that make up a trip and the photos that a user can see on the trip, etc...) and if any of that data changes, then the trip's update time also needs to change. I can't for the life of me figure out how to set up the trigger so that when I change some route information, the last updated time for the trip(s) the route belongs to will be updated in it's table.
This is my trigger code as it stands now: it updates the trip table's last updated column when that trip's row is updated.
CREATE OR REPLACE FUNCTION record_update_time() RETURNS TRIGGER AS
$$
BEGIN
NEW.last_updated=now();
RETURN NEW;
END;
$$
LANGUAGE PLPGSQL;
CREATE TRIGGER update_entry_on_entry_change
BEFORE UPDATE ON mydatabase.trip FOR EACH ROW
EXECUTE PROCEDURE record_update_time();
--I used the next two queries just to test that the trigger works. It
--probably doesn't make a difference to you but I'll keep it here for reference
UPDATE mydatabase.trip
SET title='Sample New Title'
WHERE id = 2;
SELECT *
FROM mydatabase.trip
WHERE mydatabase.trip.id < 5;
Now I need it to update when the rows referencing the trip row with a foreign key get updated. Any ideas from someone more experienced with SQL triggers than I?
"mydatabase" is a remarkably unfortunate name for a schema.
The trigger function could look like this:
CREATE OR REPLACE FUNCTION trg_upaft_upd_trip()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
UPDATE mydatabase.trip t -- "mydatabase" = schema name (?!)
SET last_updated = now()
WHERE t.id = NEW.trip_id -- guessing column names
RETURN NULL; -- calling this AFTER UPDATE
END
$func$;
And needs to be used in a trigger on every related table (not on trip itself):
CREATE TRIGGER upaft_upd_trip
AFTER UPDATE ON mydatabase.trip_detail
FOR EACH ROW EXECUTE PROCEDURE trg_upaft_upd_trip();
You also need to cover INSERT and DELETE (and possibly COPY) on all sub-tables ...
This approach has many potential points of failure. As alternative, consider a query or view that computes the latest last_updated from sub-tables dynamically. If you update often this might be the superior approach.
If you rarely UPDATE and SELECT often, your first approach might pay.