Do statements within a PL/pgSQL function run sequentially? - postgresql

I'd like to run the create_company_maybe_race_condition() function below, but think it might create a race condition if plpgsql functions don't run sequentially. I'm on Postgres 12.
My real world scenario is that I have Row Level Security turned on for all of my tables. The RLS check relies on an access control list in a permissions table. When I add a row to a table, I can't use the RETURNING clause since there isn't a row in the permissions table yet that'll allow the user to read the object.
CREATE TABLE companies (
PRIMARY KEY(company_id),
company_id uuid,
);
CREATE TABLE departments (
PRIMARY KEY (department_id),
department_id uuid DEFAULT gen_random_uuid(),
name text,
company_id uuid REFERENCES companies
);
/**********/
CREATE FUNCTION create_company_maybe_race_condition ()
RETURNS void AS $$
DECLARE
v_company_id := gen_random_uuid();
BEGIN
INSERT INTO companies (company_id)
VALUES (v_company_id);
INSERT INTO departments (company_id, name)
VALUES (v_company_id, 'My Department'); -- Postgres doesn't know that it depends on companies
END;
$$ LANGUAGE plpgsql;
/**********/
CREATE FUNCTION create_company_no_race_condition ()
RETURNS void AS $$
DECLARE
v_company_id uuid;
BEGIN
INSERT INTO companies (company_id)
VALUES (DEFAULT)
RETURN company_id INTO v_company_id;
INSERT INTO departments (company_id, name)
VALUES (v_company_id, 'My Department');
END;
$$ LANGUAGE plpgsql;

Related

Insert 2 rows in trigger (different tables, insert 2 must references insert1)

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;

Postgresql: Partitioning a table for values in SELECT

I want to use PostgreSQL's declarative table partitioning on a column with UUID values. I want it partitioned by values that are returned by a SELECT.
CREATE TABLE foo (
id uuid NOT NULL,
type_id uuid NOT NULL,
-- other columns
PRIMARY KEY (id, type_id)
) PARTITION BY LIST (type_id);
CREATE TABLE foo_1 PARTITION OF foo
FOR VALUES IN (SELECT id FROM type_ids WHERE type_name = 'type1');
CREATE TABLE foo_2 PARTITION OF foo
FOR VALUES IN (SELECT id FROM type_ids WHERE type_name = 'type2');
I don't want to use specific UUIDs in FOR VALUES IN ('uuid'), since the UUIDs may differ by environment (dev, qa, prod). However, the SELECT syntax doesn't seem to be accepted by Postgres. Any suggestions?
I just wanted the SELECT to be evaluated at the table creation time
You should have made that clear in the question, not in a comment.
In that case - if this is a one time thing, you can use dynamic SQL, e.g. wrapped into a procedure
create procedure create_partition(p_part_name text, p_type_name text)
as
$$
declare
l_sql text;
l_id uuid;
begin
select id
into l_id
from type_ids
where type_name = p_type_name;
l_sql := format('CREATE TABLE %I PARTITION OF foo FOR VALUES IN (%L)', p_part_name, l_id);
execute l_sql;
end;
$$
language plpgsql;
Then you can do:
call create_partition('foo_1', 'type1');
call create_partition('foo_2', 'type2');

How to upsert with serial primary key

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;
$$;

Is it possible to write a postgres function that will handle a many to many join?

I have a job table. I have an industries table. Jobs and industries have a many to many relationship via a join table called industriesjobs. Both tables have uuid is their primary key. My question is two fold. Firstly is it feasible to write two functions to insert data like this? If this is feasible then my second question is how do I express an array of the uuid column type. I'm unsure of the syntax.
CREATE OR REPLACE FUNCTION linkJobToIndustries(jobId uuid, industiresId uuid[]) RETURNS void AS $$
DECLARE
industryId uuid[];
BEGIN
FOREACH industryId SLICE 1 IN ARRAY industriesId LOOP
INSERT INTO industriesjobs (industry_id, job_id) VALUES (industryId, jobId);
END LOOP;
RETURN;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION insertJobWithIndistries(orginsation varchar, title varchar, addressId uuid, industryIds uuid[]) RETURNS uuid AS $$
DECLARE
jobId uuid;
BEGIN
INSERT INTO jobs ("organisation", "title", "address_id") VALUES (orginsation, title, addressId) RETURNING id INTO jobId;
SELECT JobbaLinkJobToIndustries(jobId, industryIds);
END;
$$ LANGUAGE plpgsql;
SELECT jobId FROM insertJobWithIndistries(
'Acme Inc'::varchar,
'Bomb Tester'::varchar,
'0030cfb3-1a03-4c5a-9afa-6b69376abe2e',
{ 19c2e0ee-acd5-48b2-9fac-077ad4d49b19, 21f8ffb7-e155-4c8f-acf0-9e991325784, 28c18acd-99ba-46ac-a2dc-59ce952eecf2 }
);
Thanks in advance.
Key to a solution are the function unnest() to (per documentation):
expand an array to a set of rows
And a data-modifying CTE.
A simple query does the job:
WITH ins_job AS (
INSERT INTO jobs (organisation, title, address_id)
SELECT 'Acme Inc', 'Bomb Tester', '0030cfb3-1a03-4c5a-9afa-6b69376abe2e' -- job-data here
RETURNING id
)
INSERT INTO industriesjobs (industry_id, job_id)
SELECT indid, id
FROM ins_job i -- that's a single row, so a CROSS JOIN is OK
, unnest('{19c2e0ee-acd5-48b2-9fac-077ad4d49b19
, 21f8ffb7-e155-4c8f-acf0-9e9913257845
, 28c18acd-99ba-46ac-a2dc-59ce952eecf2}'::uuid[]) indid; -- industry IDs here
Also demonstrating proper syntax for an array of uuid. (White space between elements and separators is irrelevant while not inside double-quotes.)
One of your UUIDs was one character short:
21f8ffb7-e155-4c8f-acf0-9e991325784
Must be something like:
21f8ffb7-e155-4c8f-acf0-9e9913257845 -- one more character
If you need functions, you do that, too:
CREATE OR REPLACE FUNCTION link_job_to_industries(_jobid uuid, _indids uuid[])
RETURNS void AS
$func$
INSERT INTO industriesjobs (industry_id, job_id)
SELECT _indid, _jobid
FROM unnest(_indids) _indid;
$func$ LANGUAGE sql;
Etc.
Related:
Insert data in 3 tables at a time using Postgres
How to insert multiple rows using a function in PostgreSQL

CONSTRAINT to check values from a remotely related table (via join etc.)

I would like to add a constraint that will check values from related table.
I have 3 tables:
CREATE TABLE somethink_usr_rel (
user_id BIGINT NOT NULL,
stomethink_id BIGINT NOT NULL
);
CREATE TABLE usr (
id BIGINT NOT NULL,
role_id BIGINT NOT NULL
);
CREATE TABLE role (
id BIGINT NOT NULL,
type BIGINT NOT NULL
);
(If you want me to put constraint with FK let me know.)
I want to add a constraint to somethink_usr_rel that checks type in role ("two tables away"), e.g.:
ALTER TABLE somethink_usr_rel
ADD CONSTRAINT CH_sm_usr_type_check
CHECK (usr.role.type = 'SOME_ENUM');
I tried to do this with JOINs but didn't succeed. Any idea how to achieve it?
CHECK constraints cannot currently reference other tables. The manual:
Currently, CHECK expressions cannot contain subqueries nor refer to
variables other than columns of the current row.
One way is to use a trigger like demonstrated by #Wolph.
A clean solution without triggers: add redundant columns and include them in FOREIGN KEY constraints, which are the first choice to enforce referential integrity. Related answer on dba.SE with detailed instructions:
Enforcing constraints “two tables away”
Another option would be to "fake" an IMMUTABLE function doing the check and use that in a CHECK constraint. Postgres will allow this, but be aware of possible caveats. Best make that a NOT VALID constraint. See:
Disable all constraints and table checks while restoring a dump
A CHECK constraint is not an option if you need joins. You can create a trigger which raises an error instead.
Have a look at this example: http://www.postgresql.org/docs/9.1/static/plpgsql-trigger.html#PLPGSQL-TRIGGER-EXAMPLE
CREATE TABLE emp (
empname text,
salary integer,
last_date timestamp,
last_user text
);
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
BEGIN
-- Check that empname and salary are given
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname cannot be null';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
END IF;
-- Who works for us when she must pay for it?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
END IF;
-- Remember who changed the payroll when
NEW.last_date := current_timestamp;
NEW.last_user := current_user;
RETURN NEW;
END;
$emp_stamp$ LANGUAGE plpgsql;
CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
FOR EACH ROW EXECUTE PROCEDURE emp_stamp();
...i did it so (nazwa=user name, firma = company name) :
CREATE TABLE users
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
nazwa character varying(20),
firma character varying(50)
);
CREATE TABLE test
(
id bigserial CONSTRAINT firstkey PRIMARY KEY,
firma character varying(50),
towar character varying(20),
nazwisko character varying(20)
);
ALTER TABLE public.test ENABLE ROW LEVEL SECURITY;
CREATE OR REPLACE FUNCTION whoIAM3() RETURNS varchar(50) as $$
declare
result varchar(50);
BEGIN
select into result users.firma from users where users.nazwa = current_user;
return result;
END;
$$ LANGUAGE plpgsql;
CREATE POLICY user_policy ON public.test
USING (firma = whoIAM3());
CREATE FUNCTION test_trigger_function()
RETURNS trigger AS $$
BEGIN
NEW.firma:=whoIam3();
return NEW;
END
$$ LANGUAGE 'plpgsql'
CREATE TRIGGER test_trigger_insert BEFORE INSERT ON test FOR EACH ROW EXECUTE PROCEDURE test_trigger_function();