postgres TRIGGER upsert with returning - postgresql

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,

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.

PostgreSQL Trigger Copy New Row into other Table

I have a problem I am stuck on for some time now. So I wanted to reach out for a little help.
I have 2 tables which are holding the same data: transactions and transactions2.
I want to write a Trigger that is triggering every time a new row is added to transactions and insert it into transaction2 in PLSQL.
First I simply duplicated the table with
CREATE TABLE transactions2 (SELECT * FROM transactions WHERE 1=1);
I think I found out how to insert
CREATE OR REPLACE FUNCTION copyRow RETURNS TRIGGER AS $$
DECLARE
BEGIN
INSERT INTO transaction2
VALUES transaction;
END;
I think the syntax with this is also wrong, but how do I say, that the Trigger should start as soon as a new Insert into the first table is made?
Can anyone help me with this?
Thanks
Bobby
The correct syntax for an INSERT is INSERT (<column list>) VALUES (<values list>). The INSERT syntax isn't different in a function compared to "outside". So your trigger function should look something like:
CREATE OR REPLACE FUNCTION t2t2_f ()
RETURNS TRIGGER
AS
$$
BEGIN
INSERT INTO transactions2
(column_1,
...,
column_n)
VALUES (NEW.column_1,
...,
NEW.column_n);
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
Replace the column_is with the actual column names of your table. NEW is a pseudo record with which you can access the values of the new row.
To create the trigger itself use something like:
CREATE TRIGGER t2t2_t
AFTER INSERT
ON transactions
FOR EACH ROW
EXECUTE PROCEDURE t2t2_f();
You may want to use another timing, e.g. BEFORE instead of AFTER.
That should give you something to start with. Please consider studying the comprehensive PostgreSQL Manual for further and more detailed information.

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

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

Sanitize input to a column in postgres

So, I think this should be fairly simple, but the documentation makes it seem somewhat more complicated. I've written an SQL function in PostgreSQL (8.1, for now) which does some cleanup on some string input. For what it's worth, the string is an LDAP distinguished name, and I want there to consistently be no spaces after the commas - and the function is clean_dn(), which returns the cleaned DN. I want to do the same thing to force all input to another couple of columns to lower case, etc - which should be easy once I figure this part out.
Anyway, I want this function to be run on the "dn" column of a table any time anyone attempts to insert to or update and modify that column. But all the rule examples I can find seem to make the assumption that all insert/update queries modify all the columns in a table all the time. In my situation, that is not the case. What I think I really want is a constraint which just changes the value rather than returning true or false, but that doesn't seem to make sense with the SQL idea of a constraint. Do I have my rule do an UPDATE into the NEW table? Do I have to create a new rule for every possible combination of NEW values? And if I add a column, do I have to go through and update all of my rule combinations to refelect every possible new combination of columns?
There has to be an easy way...
First, update to a current version of PostgreSQL. 8.1 is long dead and forgotten und unsupported and very, very old .. you get my point? Current version is PostgreSQL 9.2.
Then, use a trigger instead of a rule. It's simpler. It's the way most people go. I do.
For column col in table tbl ...
First, create a trigger function:
CREATE OR REPLACE FUNCTION trg_tbl_insupbef()
RETURNS trigger AS
$BODY$
BEGIN
NEW.col := f_myfunc(NEW.col); -- your function here, must return matching type
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Then use it in a trigger.
For ancient Postgres 8.1:
CREATE TRIGGER insupbef
BEFORE INSERT OR UPDATE
ON tbl
FOR EACH ROW
EXECUTE PROCEDURE trg_tbl_insupbef();
For modern day Postgres (9.0+)
CREATE TRIGGER insbef
BEFORE INSERT OR UPDATE OF col -- only call trigger, if column was updated
ON tbl
FOR EACH ROW
EXECUTE PROCEDURE trg_tbl_insupbef();
You could pack more stuff into one trigger, but then you can't condition the UPDATE trigger on just the one column ...