PL/pgSQL: Delete (update) row matching a record - postgresql

I'm writing three triggers in PL/pgSQL. In each case, I have a RECORD variable and want to insert that into a table, delete it from the table, or update it to represent a second RECORD variable.
Adding is easy: INSERT INTO mytable VALUES (NEW.*);
Deleting isn't as easy, there doesn't seem to be a syntax for something like this:
DELETE FROM mytable
WHERE * = OLD.*;
Updating has the same problem. Is there an easy solution, short of generating matching SQL queries that compare each attribute using ideas from this answer?

You can use a trick for delete
create table t(a int, b int);
create table ta(a int, b int);
create function t1_delete()
returns trigger as $$
begin
delete from ta where ta = old;
return null;
end
$$ language plpgsql;
But this trick doesn't work for UPDATE. So fully simple trigger in PL/pgSQL is not possible simply.

You write about a record variable and it is, indeed, not trivial to access individual columns of an anonymous record in plpgsql.
However, in your example, you only use OLD and NEW, which are well known row types, defined by the underlying table. It is trivial to access individual columns in this case.
DELETE FROM mytable
WHERE mytable_id = OLD.mytable_id;
UPDATE mytable_b
SET some_col = NEW.some_other_col
WHERE some_id = NEW.mytable_id;
Etc.
Just be careful not to create endless loops.
In case you just want to "update" columns of the current row, you can simply assign to columns the NEW object in the trigger. You know that, right?
NEW.some_col := 'foo';
Dynamic column names
If you don't know column names beforehand, you can still do this generically with dynamic SQL as detailed in this related answer:
Update multiple columns in a trigger function in plpgsql

Related

Create Trigger to get hourly difference between timestamps

I have a table where I would like to calculate the difference in time (in hours) between two columns after inserting a row. I would like to set up a trigger to do this whenever an insert or update is performed on the table.
My columns are delay_start, delay_stop, and delay_duration. I would like to do the following:
delay_duration = delay_stop - delay_start
The result should be of numeric (4,2) value and go into the delay_duration category. Below is what I have so far, but it will not populate the column for some reason.
BEGIN
INSERT INTO public.deckdelays(delay_duration)
VALUES(DATEDIFF(hh, delay_stop, delay_start));
RETURN NEW;
END;
I am quite new to all of this so if anyone could help I would greatly appreciate it!
If you have Postgres 12 or later you can define delay_duration as a generated column. This allows you to eliminate triggers.
create table deckdelays(id integer generated always as identity
, delay_start timestamp
, delay_stop timestamp
, delay_duration numeric(4,2)
generated always as
( extract(epoch from (delay_stop - delay_start))/3600 )
stored
--, other attributes
);
See demo here.
But if you insist on a trigger:
create or replace
function delayduration_func()
returns trigger
language plpgsql
as $$
begin
new.delay_duration = (extract(epoch from (deckdelays.delay_stop - deckdelays.delay_start))/3600)::numeric;
return new;
end;
$$;
create trigger delaydurationset1
before insert
or update of delay_stop, delay_start
on deckdelays
execute procedure delayduration_func();
Changes:
Before trigger instead of after. A before trigger can modify the
values in a column without additional DML statements, an after
trigger cannot. Issuing a DML statement on a table within a trigger
on that same table can lead to all types of problems. It is bast
avoided if possible.
Trigger name and function name not the same. Might just be me but I
do not like different things having the same name. Although it works
often leads to confusion. Always avoid confusion if possible.
Trigger fires on update of delay_start. An update of either delay_start or delay_end also updates delay_duration.

Get data of multiple inserted rows in one object using trigger in postgres

I am trying to write a trigger which gets data from the table attribute in which multiple rows are inserted corresponding to one actionId at one time and group all that data into the one object:
Table Schema
actionId
key
value
I am firing trigger on rows insertion,SO how can I handle this multiple row insertion and how can I collect all the data.
CREATE TRIGGER attribute_changes
AFTER INSERT
ON attributes
FOR EACH ROW
EXECUTE PROCEDURE log_attribute_changes();
and the function,
CREATE OR REPLACE FUNCTION wflowr222.log_task_extendedattribute_changes()
RETURNS trigger AS
$BODY$
DECLARE
_message json;
_extendedAttributes jsonb;
BEGIN
SELECT json_agg(tmp)
INTO _extendedAttributes
FROM (
-- your subquery goes here, for example:
SELECT attributes.key, attributes.value
FROM attributes
WHERE attributes.actionId=NEW.actionId
) tmp;
_message :=json_build_object('actionId',NEW.actionId,'extendedAttributes',_extendedAttributes);
INSERT INTO wflowr222.irisevents(message)
VALUES(_message );
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
and data format is,
actionId key value
2 flag true
2 image http:test.com/image
2 status New
I tried to do it via Insert trigger, but it is firing on each row inserted.
If anyone has any idea about this?
I expect that the problem is that you're using a FOR EACH ROW trigger; what you likely want is a FOR EACH STATEMENT trigger - ie. which only fires once for your multi-line INSERT statement. See the description at https://www.postgresql.org/docs/current/sql-createtrigger.html for a more through explanation.
AFAICT, you will also need to add REFERENCING NEW TABLE AS NEW in this mode to make the NEW reference available to the trigger function. So your CREATE TRIGGER syntax would need to be:
CREATE TRIGGER attribute_changes
AFTER INSERT
ON attributes
REFERENCING NEW TABLE AS NEW
FOR EACH STATEMENT
EXECUTE PROCEDURE log_attribute_changes();
I've read elsewhere that the required REFERENCING NEW TABLE ... syntax is only supported in PostgreSQL 10 and later.
Considering the version of postgres you have, and therefore keeping in mind that you can't use a trigger defined FOR EACH STATEMENT for your purpose, the only alternative I see is
using a trigger after insert in order to collect some information about changes in a utility table
using a unix cron that execute a pl/sql that do the job on data set
For example:
Your utility table
CREATE TABLE utility (
actionid integer,
createtime timestamp
);
You can define a trigger FOR EACH ROW with a body that do something like this
INSERT INTO utilty values(NEW.actionid, curent_timestamp);
And, finally, have a crontab UNIX that execute a file or a procedure that to something like this:
SELECT a.* FROM utility u JOIN yourtable a ON a.actionid = u.actionid WHERE u.createtime < current_timestamp;
// do something here with records selected above
TRUNCATE table utility;
If you had postgres 9.5 you could have used pg_cron instead of unix cron...

postgres TRIGGER upsert with returning

i want an upsert functionality that returns the (new/existing) id of the row.
Linking to my previous question. I asked previously about RULE postgres create rule on insert do nothing if exists insert otherwise; RETURNING id but looks like it is not possible.
So I resort to a trigger
CREATE OR REPLACE FUNCTION upsert_asset() RETURNS trigger AS $trigger_bound$
BEGIN
INSERT INTO asset(symbol, name, type, status)
VALUES (NEW.symbol, NEW.name, NEW.type, NEW.status)
ON CONFLICT (symbol) DO UPDATE SET symbol = EXCLUDED.symbol;
RETURN NEW;
END;
$trigger_bound$
LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER upsert_asset_trigger
AFTER INSERT ON asset
FOR EACH ROW
EXECUTE PROCEDURE upsert_asset();
I tested the above and works. So my questions are
Is this trigger a correct way to achieve this functionality? Any race conditions/performance issues that i should know about?
How can I generalize this query, by not giving the column names? asset(symbol, name, type, status). i do not want to pay attention to this rule every time I change my table. Is it possible to say NEW.* or column.* or something? What psuedorelations are available to achieve this? Please note there are some default columns too. So how does NEW.default_column get a value incase the insert statement has left that column in the insert statement?
Thanks,

PLPGSQL function returning trigger AND value

I have written a PL/PGSQL function that returns a trigger, so I can call it before each row insert. I realize now that I would also like that function to return the ID of the newly inserted row. I'm not quite sure how to proceed since my function must return a trigger. Here's some code:
CREATE OR REPLACE FUNCTION f_insert_album() RETURNS TRIGGER AS $$
DECLARE
subj_album_id INTEGER;
BEGIN
-- ... some parts where left out
INSERT INTO t_albums_subjective (user_id, album_id, format_id, location_id, rating)
VALUES (NEW.user_id, obj_album_id, NEW.format_id, NEW.location_id, NEW.rating)
RETURNING id INTO subj_album_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Bind insert function to trigger
DROP TRIGGER IF EXISTS tr_v_albums_insert ON v_albums;
CREATE TRIGGER tr_v_albums_insert INSTEAD OF INSERT ON v_albums
FOR EACH ROW EXECUTE PROCEDURE f_insert_album();
I must keep the return type of my function f_insert_album() to TRIGGER, but I would really like to also return the value in subj_album_id, corresponding to the id of the newly inserted row.
Is there anyway to do this? Is it even possible? Obviously changing the return type didn't work with Postgres. Could you suggest an alternative approach if any?
The crucial question: where to return the ID to?
Assuming you want to return it from the INSERT statement directly, then you are almost there. You already assign the newly generated ID to a function parameter:
...
RETURNING id INTO subj_album_id;
Instead, assign it to a column of the row firing the trigger. The special variable NEW holds this row in a trigger function:
...
RETURNING id INTO NEW.album_id; -- use actual column name in view
Then use the RETURNING clause of the INSERT statement:
INSERT INTO v_albums (user_id, format_id, location_id, rating)
VALUES ( ... )
RETURNING album_id;
Obviously, this is only possible if there is a visible column in the view. It does not have to be assigned in the INSERT command, though. The type of the NEW row variable is defined by the definition of the view, not by the INSERT at hand.
Closely related:
RETURNING data from updatable view not working?
For what you seem to be doing (grant access to certain rows of a table to a certain role) row level security (RLS) in Postgres 9.5 or later might be a more convenient alternative:
CREATE POLICY in the manual
The Postgres Wiki: "What's new in PostgreSQL 9.5"
Review by Depesz

Calculate a column value during INSERT

I am using Postgresql 8.3
I have a Database table contaning buy_value and sell_value. I wish to add a DEFAULT function so that on Every Insert, database will calculate the profit according to the buy and sell values and insert that to the related column...
How should I define my alter?
it's not as simple as changing the DEFAULT value... you'll need a trigger before insert. (but its not recomended to store calculated fields on the table, because you risk to break the data integrity)
check this out
http://www.postgresql.org/docs/9.1/static/triggers.html
Good Luck
I can't really think of a good reason to store such computed column in postgres.
In terms of speed of writing and reading - additional I/O will produce performance hit which can very hardly be justified (maybe in most CPU bound systems, but even then for such trivial operation it would not make sense).
Usually storing computed columns is necessary to create an index, however postgres has functional indexes, so you can create an index without having a materialized column and for the rest of the purposes use a view.
drop table foo2 cascade;
create table foo2 (val int, valplus int);
create or replace function fooplusonetrig()
returns trigger as $$
declare
rec record;
begin
raise notice 'here with % %',new.val,new.valplus;
new.valplus := new.val + 1;
return new;
end; $$ language 'plpgsql';
create trigger fooplusonetrig before insert ON foo2 for each row execute procedure fooplusonetrig();
insert into foo2 values (2,1);
select * from foo2;
val | valplus
-----+---------
2 | 3