I have the following procedure:
CREATE OR REPLACE FUNCTION create_user(p_email character varying) RETURNS integer AS
$$
DECLARE
v_user_id integer;
BEGIN
SELECT user_id FROM user WHERE email = p_email INTO v_user_id;
IF v_user_id IS NULL THEN
INSERT
INTO user (email, status)
VALUES (p_email, 'active')
RETURNING user_id INTO v_user_id;
END IF;
RETURN v_user_id;
END;
$$ LANGUAGE plpgsql;
But if there multiple parallel requests to DB, it causes race condition, the procedure gets called multiple times and since the delay is so low, all of them trying to create a user and only one of them is succeeding. Is there any good workaround for this?
Is there any good workaround for this?
Yes, create a unique index on the email column, then use insert on conflict:
...
BEGIN
INSERT INTO "user" (email, status)
VALUES (p_email, 'active')
ON CONFLICT (email) DO NOTHING
RETURNING user_id INTO v_user_id;
RETURN v_user_id;
END;
The above will however not return the user_id if the email already exists. If you need that, you will need something like this:
...
BEGIN
INSERT INTO "user" (email, status)
VALUES (p_email, 'active')
ON CONFLICT (email) DO NOTHING
RETURNING user_id INTO v_user_id;
if v_user_id is null then
-- no insert happened
select user_id
into v_user_id
from "user"
where email = p_email;
end if;
RETURN v_user_id;
END;
Related
I need to insert 2 rows into 2 different tables using a trigger but the public.user table needs to get the company id that I am inserting into the db within the same trigger. Is this possible?
My other option (i think) would be to add another trigger for after inserts on public.user and update the public user created_by column using a select from the company table.
create or replace function public.handle_new_user()
returns trigger as $$
begin
-- Insert here
insert into public.company (created_by)
values (new.id)
-- need to ref public.company just inserted in this insert statement
insert into public.users (id, email, name, company_id)
values (new.id, new.email, new.raw_user_meta_data->>'full_name', (select top 1 id from public.company where created_by = new.id));
return null;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
Postgres provides the ability to return columns from the INSERT. With that you can get the id of the company row just inserted.
create or replace function public.handle_new_user()
returns trigger as $$
declare
l_company_id public.company.id%type;
begin
-- Insert here
insert into public.company (created_by)
values (new.id)
returning id into l_company_id;
-- need to ref public.company just inserted in this insert statement
insert into public.users (id, email, name, company_id)
values (new.id, new.email, new.raw_user_meta_data->>'full_name', l_company_id);
return null;
end;
$$ language plpgsql security definer;
My tables are
CREATE TABLE historia_alumno(
period varchar(5) NOT NULL,
no_control varchar(9) NOT NULL,
mater varchar(7) NOT NULL,
calif int
primary key(no_control,mater)
);
as unique condition; also
CREATE TABLE seleccion_mater(
period varchar(5) NOT NULL,
no_control varchar(9) NOT NULL,
mater varchar(7) NOT NULL
primary key(period,no_control,mater)
);
with unique condition.
If ever looks similar, has different scenarios; my trigger function was created with pgadmin4 and is as follows:
CREATE OR REPLACE FUNCTION alta_materia()
RETURNS trigger
LANGUAGE 'plpgsql'
VOLATILE
COST 100
AS $BODY$
BEGIN
IF EXISTS(SELECT 1 FROM historia_alumno HA WHERE HA.no_control=NEW.no_control AND HA.mater=NEW.mater AND HA.calif>=70)THEN
RAISE EXCEPTION 'Student has approved previously assignment';
ROLLBACK;
RETURN NULL;
ELSE
IF EXISTS(SELECT 1 FROM seleccion_mater SM WHERE SM.period=NEW.period AND SM.no_control=NEW.no_control AND SM.mater=NEW.mater)THEN
RAISE EXCEPTION 'Student has assignment in their list of to_do ';
ROLLBACK;
RETURN NULL;
END IF;
RETURN NEW;
END IF;
END;
$BODY$;
This function is activated before insert in table seleccion_mater.
WITHOUT insert any value, and as a check point, i've done the follow (in pgadmin4)
do $$
BEGIN
IF EXISTS(SELECT 1 FROM seleccion_mater SM WHERE SM.period='20221' and SM.no_control='17760218' AND SM.materi='05ISC04') THEN
RAISE NOTICE 'Exists';
ELSE
RAISE NOTICE 'Not exists';
END IF;
END $$;
Output is "Not exists", and that is correct because the table is empty; but, if i now do
INSERT INTO seleccion_mater(period,no_control,mater) VALUES('20221','17760218','05ISC04');
Result is
Error: Student has assignment in their list of to_do
When value is not present in the table.
I've also change the trigger function as follow
DECLARE
yeap integer:=0;
SELECT COUNT(*) INTO yeap FROM seleccion_mater SM WHERE SM.period=NEW.period AND SM.no_control=NEW.no_control AND SM.mater=NEW.mater;
IF yeap>0 THEN
RAISE EXCEPTION 'Student has assignment in their to_do list %',yeap;
And then, trying to do insert again with previously values as before, the output is now
Student has assignment in their to_do list 1
I don't known why is counting a value the trigger function, when is not present.
I have a stored procedure to perform an upsert. However the conflict condition never runs, passing an existing ID always causes it to create a new record.
create or replace function upsert_email(v_id bigint, v_subject character varying)
returns bigint
language plpgsql
as $$
declare
v_id bigint;
begin
insert into emails
(id, subject)
values (coalesce(v_id, (select nextval('serial'))), v_subject)
on conflict(id)
do update set (subject) = (v_subject) where emails.id = v_id
returning id into v_id;
return v_id;
end;
$$;
When running select upsert_email(6958500, 'subject'); which is a record that exists, it always creates a new record instead.
I have already looked at: Upsert/on conflict with serial primary key which is the most similar question and is what my SQL is modeled on, however I haven't been able to get it to work.
Wow, idiot of the year award goes to me.
I'm taking a parameter called v_id, then immediately overwrite it in the declare with a v_id; They should be named 2 different things:
create or replace function upsert_email(v_id bigint, v_subject character varying)
returns bigint
language plpgsql
as $$
declare
v_return_id bigint;
begin
insert into emails
(id, subject)
values (coalesce(v_id, (select nextval('serial'))), v_subject)
on conflict(id)
do update set (subject) = (v_subject) where emails.id = v_id
returning id into v_return_id;
return v_return_id;
end;
$$;
I am pretty new to PL/pgSQL programming. I have a requirement of audit logging updated columns in my table
Table
create table sample_table(name varchar(15),city varchar(15),age int,mail varchar(20) primary key);
Audit table
create table sample_table__audits_dynamicols(mail varchar(20), columnchanged varchar(10), oldvalue varchar(10), changed_on timestamp(6) NOT NULL)
Trigger Function
CREATE FUNCTION public.log_sample_table_allchanges() RETURNS trigger AS $BODY$DECLARE
_colname text;
_tablename varchar(15) := 'sample_table';
_schema varchar(15) := 'public';
_changed_on time := now();
BEGIN
FOR _colname IN SELECT column_name FROM information_schema.Columns WHERE table_schema = _schema AND table_name = _tablename LOOP
IF NEW._colname <> OLD._colname THEN
INSERT INTO sample_table__audits_dynamicols(mail,columnchanged, oldvalue ,changed_on)
VALUES(OLD.mail,_colname,OLD.:_colname,_changed_on);
END IF;
END LOOP;
RETURN NEW;
END$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF;
Trigger
create TRIGGER log_sample_table_allchanges
BEFORE UPDATE
ON SAMPLE_TABLE
FOR EACH ROW
EXECUTE PROCEDURE log_sample_table_allchanges();
Requirement: Whenever a column value is changed i want to log it as
(mail, columnname, columnvalue, date)
E.g:
insert into sample_table (name, mail, city, age) values('kanta','mk#foo.com','hyd',23);
insert into sample_table (name, mail, city, age) values('kmk','mk#gmail.com','hyd',23);
So when i update like the following
update sample_table set age=24 where mail='mk#foo.com';
update sample_table set city='bza' where mail='mk#gmail.com'
I want audit table to record like
(mk#foo.com,age,23, timestamp)
(mk#gmail.com, city, hyd, timestamp)
Right now I am facing issue with column comparison in my Trigger function. Please help me rectifying my Trigger function to meet my requirement.
You may use EXECUTE to get the values of columns dynamically and do the comparison.
CREATE OR REPLACE FUNCTION public.log_sample_table_allchanges() RETURNS trigger AS
$BODY$
DECLARE
_colname text;
_tablename varchar(15) := 'sample_table';
_schema varchar(15) := 'public';
_changed_on timestamp := now();
_old_val text;
_new_val text;
BEGIN
FOR _colname IN SELECT column_name FROM information_schema.Columns WHERE table_schema = _schema AND table_name = _tablename
LOOP
EXECUTE 'SELECT $1.' || _colname || ', $2.' || _colname
USING OLD,NEW
INTO _old_val, _new_val; --get the old and new values for the column.
IF _new_val <> _old_val THEN
INSERT INTO sample_table__audits_dynamicols(mail,columnchanged, oldvalue ,changed_on)
VALUES(OLD.mail,_colname,_old_val,_changed_on);
END IF;
END LOOP;
RETURN NEW;
END$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF;
I'm not sure why you have defined mail as a PRIMARY KEY in the audits table, it will cause unique constraint violation if the same mail gets updated twice.
I am referring to http://www.if-not-true-then-false.com/2009/11/howto-create-postgresql-table-partitioning-part-1/
To reproduce the problem, here is some simple steps to follow :
(1) create database named "tutorial"
(2) perform the following SQL query :
CREATE TABLE impressions_by_day (
advertiser_id SERIAL NOT NULL,
day DATE NOT NULL DEFAULT CURRENT_DATE,
impressions INTEGER NOT NULL,
PRIMARY KEY (advertiser_id, day)
);
CREATE OR REPLACE FUNCTION insert_table()
RETURNS void AS
$BODY$DECLARE
_impressions_by_day impressions_by_day;
BEGIN
INSERT INTO impressions_by_day(impressions ) VALUES(888) RETURNING * INTO _impressions_by_day;
RAISE NOTICE 'After insert, the returned advertiser_id is %', _impressions_by_day.advertiser_id;
END;$BODY$
LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION insert_table() OWNER TO postgres;
(3) create database named "tutorial_partition"
(4) perform the following SQL query :
CREATE TABLE impressions_by_day (
advertiser_id SERIAL NOT NULL,
day DATE NOT NULL DEFAULT CURRENT_DATE,
impressions INTEGER NOT NULL,
PRIMARY KEY (advertiser_id, day)
);
CREATE OR REPLACE FUNCTION insert_table()
RETURNS void AS
$BODY$DECLARE
_impressions_by_day impressions_by_day;
BEGIN
INSERT INTO impressions_by_day(impressions ) VALUES(888) RETURNING * INTO _impressions_by_day;
RAISE NOTICE 'After insert, the returned advertiser_id is %', _impressions_by_day.advertiser_id;
END;$BODY$
LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION insert_table() OWNER TO postgres;
CREATE TABLE impressions_by_day_y2010m1ms2 (
PRIMARY KEY (advertiser_id, day),
CHECK ( day >= DATE '2010-01-01' AND day < DATE '2010-03-01' )
) INHERITS (impressions_by_day);
CREATE INDEX impressions_by_day_y2010m1ms2_index ON impressions_by_day_y2010m1ms2 (day);
CREATE OR REPLACE FUNCTION impressions_by_day_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF ( NEW.day >= DATE '2010-01-01' AND NEW.day < DATE '2010-03-01' ) THEN
INSERT INTO impressions_by_day_y2010m1ms2 VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Date out of range. Something wrong with the impressions_by_day_insert_trigger() function!';
END IF;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER insert_impressions_by_day_trigger
BEFORE INSERT ON impressions_by_day
FOR EACH ROW EXECUTE PROCEDURE impressions_by_day_insert_trigger();
(5) execute
SELECT * FROM insert_table() on tutorial
We get
NOTICE: After insert, the returned advertiser_id is 1
(6) execute
SELECT * FROM insert_table() on tutorial_partition
We get
NOTICE: After insert, the returned advertiser_id is
How is it possible to get advertiser_id is 1 too, in tutorial_partition?
The trigger is passing back NULL, which indicates to the INSERT that no action is to be performed. Since no action is performed, the RETURNING * clause returns nothing. You're not going to be able to intercept (and override) the INSERT and use RETURNING in the same operation and get anything meaningful.