Rollback trigger on insertion conflict - sql

I had this :
CREATE FUNCTION upsert_user(u_name text, u_fullname text, u_email text, u_suffix text) RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
userid users.id_user%TYPE;
BEGIN
LOOP
-- first try to update
UPDATE users SET "fullname" = u_fullname, "email" = u_email, "suffix" = u_suffix WHERE "name" = u_name RETURNING "id_user" INTO userid;
-- check if the row is found
IF FOUND THEN
RETURN userid;
END IF;
-- not found so insert the row
BEGIN
INSERT INTO users ("name", "fullname", "email", "suffix") VALUES (u_name, u_fullname, u_email, u_suffix) RETURNING "id_user" INTO userid;
RETURN userid;
EXCEPTION WHEN unique_violation THEN
-- do nothing and loop
END;
END LOOP;
END;
$$;
CREATE TRIGGER link_entity
BEFORE INSERT
ON public.users
FOR EACH ROW
EXECUTE PROCEDURE public.link_entity();
CREATE FUNCTION link_entity() RETURNS trigger
LANGUAGE plpgsql
AS $$ DECLARE
entityid integer;
BEGIN
INSERT INTO privileges_entities (name) VALUES (NEW.name) RETURNING privileges_entities.id_entity INTO entityid;
IF NOT FOUND THEN
RETURN NULL;
END IF;
NEW.ref_entity := entityid;
RETURN NEW;
END;
$$;
After updated postgresql to version 9.5, I modified the function upsert_user to use the new instruction ON CONFLICT:
CREATE FUNCTION upsert_user(u_name text, u_fullname text, u_email text, u_suffix text) RETURNS integer
LANGUAGE sql
AS $$
INSERT INTO users (name, fullname, email, suffix)
VALUES (u_name, u_fullname, u_email, u_suffix)
ON CONFLICT (name) DO UPDATE SET name=EXCLUDED.name, fullname=EXCLUDED.fullname, email=EXCLUDED.email, suffix=EXCLUDED.suffix
RETURNING id_user;
$$;
The problem is that, now, new rows are inserted in the privileges_entities table even if insertion into the users table fails.
Is it possible to rollback the trigger if the insertion of the user leads to a conflict?

This is indeed a side-effect of using the new ON CONFLICT clause.
My solution here would be to add a check into the link_entity() function itself and prevent it from continuing if the user already exists. Like this:
CREATE FUNCTION link_entity() RETURNS trigger
LANGUAGE plpgsql
AS $$ DECLARE
entityid integer;
nameExists boolean;
BEGIN
EXECUTE format('SELECT EXISTS(SELECT 1 FROM %I.%I WHERE name = NEW.name)', TG_TABLE_SCHEMA, TG_TABLE_NAME) INTO nameExists;
IF nameExists THEN
RETURN NEW; -- just return, entity already linked
END IF;
INSERT INTO privileges_entities (name) VALUES (NEW.name) RETURNING privileges_entities.id_entity INTO entityid;
IF NOT FOUND THEN
RETURN NULL;
END IF;
NEW.ref_entity := entityid;
RETURN NEW;
END;
$$;

Related

Update columns for the first user inserted

I'm trying to create a trigger and a function to update some columns (roles and is_verified) for the first user created. Here is my function :
CREATE OR REPLACE FUNCTION public.first_user()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
begin
if(select count(*) from public.user = 1) then
update new set new.is_verified = true and new.roles = ["ROLE_USER", "ROLE_ADMIN"]
end if;
return new;
end;
$function$
;
and my trigger :
create trigger first_user
before insert
on
public.user for each row execute function first_user()
I'm working on Dbeaver and Dbeaver won't persist my function because of a syntax error near the "=". Any idea ?
Quite a few things are wrong in your trigger function. Here it is revised w/o changing your business logic.
However this will affect the second user, not the first. Probably you shall compare the count to 0. Then the condition shall be if not exists (select from public.user) then
CREATE OR REPLACE FUNCTION public.first_user()
RETURNS trigger LANGUAGE plpgsql AS
$function$
begin
if ((select count(*) from public.user) = 1) then
-- probably if not exists (select from public.user) then
new.is_verified := true;
new.roles := array['ROLE_USER', 'ROLE_ADMIN'];
end if;
return new;
end;
$function$;

Postgres SQL function and trigger to insert record in another table

I need help to complete the following requirements:
[x] Profile table with columns id, username, ...
[x] Comment table with columns id, content, ...
[x] CommentReference table with columns id, profile_id, comment_id, ...
When a new comment is created, before inserting:
[ ] Check if NEW.content have references to usernames, like #someusername
[ ] Check if each reference exists in the profile table
[ ] For references that exist, insert into CommentReferences the profile and comment
For now, what I have is the following code:
PS: the following code has errors, I need help to fix it. I'm using postgres version 12.
CREATE FUNCTION create_comment_usernames_references()
RETURNS trigger AS $$
DECLARE usernames TEXT[];
DECLARE username TEXT;
DECLARE profile_id TEXT; -- profile_id is of type uuid, is it correct to use TEXT here?
BEGIN
-- verify if there are usernames in the comment.content with the username regex
SELECT DISTINCT(
regexp_matches(
NEW.content,
'#(([a-z0-9]*((?<=[a-z0-9])[-|_|\.](?=[a-z0-9]))[a-z0-9]*)*|[a-z0-9]*)',
'g'
)
)[1]
INTO usernames;
FOREACH username IN ARRAY usernames LOOP
SELECT (SELECT id FROM "public"."Profile" WHERE "username" = username) INTO profile_id
INSERT INTO "public"."CommentReference" (comment_id, profile_id) VALUES (NEW.id, profile_id);
END LOOP;
-- return nothing
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER create_comment_usernames_references_trigger
BEFORE INSERT OR UPDATE ON "public"."Comment"
FOR EACH ROW
EXECUTE PROCEDURE create_comment_usernames_references();
Solve it myself.
Final code:
CREATE OR REPLACE FUNCTION find_profile_ids_by_usernames_in_text(_value TEXT)
RETURNS TABLE (profile_id uuid) AS
$func$
BEGIN
RETURN QUERY
SELECT
id
FROM
"public"."Profile"
WHERE
username IN (
SELECT DISTINCT(
regexp_matches(
_value,
'#(([a-z0-9]*((?<=[a-z0-9])[-|_|\.](?=[a-z0-9]))[a-z0-9]*)*|[a-z0-9]*)',
'g')
)[1]
);
END
$func$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION create_comment_reference(_comment_id UUID, _content TEXT)
RETURNS integer AS $$
DECLARE
row RECORD;
BEGIN
FOR row IN
SELECT * FROM find_profile_ids_by_usernames_in_text(_content)
LOOP
INSERT INTO
"public"."CommentReference" (comment_id, profile_id)
VALUES
(_comment_id, row.profile_id)
ON CONFLICT DO NOTHING;
END LOOP;
RETURN 1;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION create_comment_reference_trigger_func()
RETURNS trigger AS $$
DECLARE someval integer;
BEGIN
select * from create_comment_reference(NEW.id, NEW.content) into someval;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER create_comment_usernames_references_trigger
AFTER INSERT OR UPDATE ON "public"."Comment"
FOR EACH ROW
EXECUTE PROCEDURE create_comment_reference_trigger_func();

Update trigger not firing in plpgsql

I have an update trigger and insert trigger on a table nums with two columns namely name,number.Whenever data is updated or inserted then these triggers are executed.
CREATE OR REPLACE FUNCTION add_log()
RETURNS trigger AS
$BODY$
DECLARE
account_type varchar;
BEGIN
IF (TG_OP = 'INSERT') THEN
INSERT INTO log
VALUES(
now(),
'inserted data is name : '||NEW.name||' num : '||NEW.number
);
RETURN NEW;
ELSEIF (TG_OP = 'update') THEN
INSERT INTO log VALUES
(
now(),
'updated record with old num :'||OLD.number||' with new num : '||NEW.number
);
RETURN OLD;
END IF;
RETURN null;
END;
$BODY$
LANGUAGE plpgsql
creating trigger:
CREATE TRIGGER add_log_trigger
AFTER INSERT OR UPDATE
ON nums
FOR EACH ROW
EXECUTE PROCEDURE add_log();
When an insert operation is performed, insert trigger is being fired.But when update operation is performed no update trigger is being fired.Why?
String comparison is case-sensitive in Postgres. Try TG_OP = 'UPDATE' instead of TG_OP = 'update'.

create a trigger before insert

I want to create a trigger before insert and check inside the trigger function the summary_id values of the new row to be inserted, to see if the summary_id already exists in the table. if it exists then the trigger should return null as I don't want to insert duplicate values. I have written this function, but, when I tried to add a new duplicate row, it was inserted successfully!
CREATE OR REPLACE FUNCTION trigger_ack_bi()
RETURNS trigger AS $$
--DECLARE
-- max_points INTEGER;
BEGIN
IF(TG_OP = 'INSERT') THEN
IF NEW.ack_summary_id = (SELECT ack_summary_id FROM scm_main.tbl_ack WHERE scm_main.tbl_ack.ack_summary_id = NEW.ack_summary_id LIMIT 1) THEN
RETURN NULL;
ELSE
RETURN NEW;
END IF;
END IF;
END;
$$LANGUAGE plpgsql;
CREATE TRIGGER trigger_ack_bi BEFORE INSERT ON
scm_main.tbl_ack
FOR EACH ROW
EXECUTE PROCEDURE trigger_ack_bi()
ALTER FUNCTION trigger_ack_bi() OWNER TO postgres;
I'd recommend setting column to be NOT NULL and creating a PK on it...
alter table scm_main.tbl_ack alter column ack_summary_id set NOT NULL;
alter table scm_main.tbl_ack add primary key (ack_summary_id);
it seems the trigger function is correct. But, it is running in silent mode. raising an exception was what I needed to make sure that duplication had been prevented. Thank you for your other solutions.
CREATE OR REPLACE FUNCTION trigger_ack_bi()
RETURNS trigger AS $$
--DECLARE
-- max_points INTEGER;
BEGIN
IF(TG_OP = 'INSERT') THEN
IF NEW.ack_summary_id IS NULL THEN
RAISE EXCEPTION 'summaryID cannot be null';
END IF;
IF NEW.ack_summary_id = (SELECT ack_summary_id FROM scm_main.tbl_ack WHERE scm_main.tbl_ack.ack_summary_id = NEW.ack_summary_id LIMIT 1) THEN
RAISE EXCEPTION 'duplication prevented..';
RETURN NULL;
ELSE
RETURN NEW;
END IF;
END IF;
END;
$$LANGUAGE plpgsql;
CREATE TRIGGER trigger_ack_bi BEFORE INSERT ON
scm_main.tbl_ack
FOR EACH ROW
EXECUTE PROCEDURE trigger_ack_bi()

User-defined function with bind params

I am using the Postgres upsert example. I can get it to work as shown in the example but I need to make the function call be dynamic. The function is
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
LOOP
UPDATE db SET b = data WHERE a = key;
IF found THEN
RETURN;
END IF;
BEGIN
INSERT INTO db(a,b) VALUES (key, data);
RETURN;
EXCEPTION WHEN unique_violation THEN
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
I can get it to work this way:
SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');
But I would like to do something like:
SELECT merge_db($1,$2);
Is this possible? I know I can do this by concatenating strings but I would like to prepare my statement and use bind params.
Not sure why I didn't think to try this before but here's the answer:
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
CREATE FUNCTION merge_db(INT, TEXT) RETURNS VOID AS
$$
BEGIN
LOOP
UPDATE db SET b = $2 WHERE a = $1;
IF found THEN
RETURN;
END IF;
BEGIN
INSERT INTO db(a,b) VALUES ($1, $2);
RETURN;
EXCEPTION WHEN unique_violation THEN
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
With that you can use
SELECT merge_db($1, $2)