Race Condition between SELECT and INSERT for multiple columns - sql

Note: This is a question which is a follow up of this solution. You need to read the link to get context for this question. Also, this is for postgres v9.4
If we want to return multiple columns now instead of just 1 column, how can we achieve it?
Let's take a table t:
create table t(tag_id int, tag text unique);
Now this is what I want:
whenever I call a method f_tag_id, I want it to return all the columns for the unique row if it exists in the table t else insert it and return all the columns.
So these are the things I tried for the f_insert_tag
Option1:
CREATE OR REPLACE FUNCTION f_insert_tag(tag_p_id int, _tag text)
RETURNS TABLE(_tag_p_id int, _tag_ text)
AS
$func$
BEGIN
INSERT INTO t(tag_id,tag) VALUES (tag_p_id, _tag);
return query Select * from t where t.tag_id = tag_p_id;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- catch exception, NULL is returned
END
$func$ LANGUAGE plpgsql;
Option 2:
CREATE OR REPLACE FUNCTION f_insert_tag(tag_p_id int, _tag text, out _tag_id int, out _tag_ text)
AS
$func$
BEGIN
INSERT INTO t(tag_id,tag) VALUES (tag_p_id, _tag);
Select t.tag_id, t.tag from t where t.tag_id = tag_p_id into _tag_id, _tag_;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- catch exception, NULL is returned
END
$func$ LANGUAGE plpgsql;
Option 3:
CREATE OR REPLACE FUNCTION f_insert_tag(tag_p_id int, _tag text)
returns setof t AS
$func$
BEGIN
INSERT INTO t(tag_id,tag) VALUES (tag_p_id, _tag);
return query Select * from t where t.tag_id = tag_p_id;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- catch exception, NULL is returned
END
$func$ LANGUAGE plpgsql;
All the 3 worked by themselves:
select f_insert_tag(1322, 'helloworldaa');
f_insert_tag
---------------------
(1322,helloworldaa)
For the other function, f_tag_id as well I tried many methods:
Option 1:
CREATE OR REPLACE FUNCTION f_tag_id(tag_p_id int, _tag text, out _tag_id int, out _tag_ text)
AS
$func$
BEGIN
LOOP
SELECT t.tag_id, t.tag FROM t WHERE t.tag = _tag
UNION ALL
SELECT f_insert_tag(tag_p_id, _tag)
into _tag_id, _tag_;
EXIT WHEN _tag_id IS NOT NULL; -- else keep looping
END LOOP;
END
$func$ LANGUAGE plpgsql;
Option 2:
CREATE OR REPLACE FUNCTION f_tag_id(tag_p_id int, _tag text)
RETURNS table(_tag_id int, _tag_ text) AS
$func$
BEGIN
LOOP
SELECT t.tag_id, t.tag FROM t WHERE t.tag = _tag
UNION ALL
SELECT f_insert_tag(tag_p_id, _tag)
into _tag_id, _tag_;
EXIT WHEN _tag_id IS NOT NULL; -- else keep looping
END LOOP;
END
$func$ LANGUAGE plpgsql;
For both these, I got the same error:
select f_tag_id(22, 'test');
ERROR: each UNION query must have the same number of columns
LINE 3: SELECT f_insert_tag(tag_p_id, _tag)
^
QUERY: SELECT t.tag_id, t.tag FROM t WHERE t.tag = _tag
UNION ALL
SELECT f_insert_tag(tag_p_id, _tag)
CONTEXT: PL/pgSQL function f_tag_id(integer,text) line 5 at SQL statement

The wrench in the works is SELECT f_insert_tag(tag_p_id, _tag) instead of
SELECT * FROM f_insert_tag(tag_p_id, _tag)
For Postgres 9.4
CREATE FUNCTION f_insert_tag(_tag_id int, _tag text, OUT _tag_id_ int, OUT _tag_ text)
AS
$func$
BEGIN
INSERT INTO t(tag_id, tag)
VALUES (_tag_id, _tag)
RETURNING t.tag_id, t.tag
INTO _tag_id_, _tag_;
EXCEPTION WHEN UNIQUE_VIOLATION THEN
-- catch exception, return NULL
END
$func$ LANGUAGE plpgsql;
CREATE FUNCTION f_tag_id(_tag_id int, _tag text, OUT _tag_id_ int, OUT _tag_ text) AS
$func$
BEGIN
LOOP
SELECT t.tag_id, t.tag
FROM t
WHERE t.tag = _tag
UNION ALL
SELECT * -- !!!
FROM f_insert_tag(_tag_id, _tag)
LIMIT 1
INTO _tag_id_, _tag_;
EXIT WHEN _tag_id_ IS NOT NULL; -- else keep looping
END LOOP;
END
$func$ LANGUAGE plpgsql;
db<>fiddle here
For Postgres 9.5 or later:
CREATE FUNCTION f_tag_id(_tag_id int, _tag text, OUT _tag_id_ int, OUT _tag_ text) AS
$func$
BEGIN
LOOP
SELECT t.tag_id, t.tag
FROM t
WHERE t.tag = _tag
INTO _tag_id_, _tag_;
EXIT WHEN FOUND;
INSERT INTO t (tag_id, tag)
VALUES (_tag_id, _tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id, t.tag
INTO _tag_id_, _tag_;
EXIT WHEN FOUND;
END LOOP;
END
$func$ LANGUAGE plpgsql;
db<>fiddle here
Basics here:
Is SELECT or INSERT in a function prone to race conditions?

Related

How to recursively return table in postgres function using plpgsql? Without using CTE/With Recursive

I tried implementing it here as follows
create or replace function getTextEditRecord(textId integer)
RETURNS Table (
text_id integer,
text_details character varying,
new_text_id integer) AS $$
DECLARE
curr_rec record;
temp_rec record;
BEGIN
curr_rec :=
(select tm.text_id, tm.text_details, tm.new_text_id from text_master tm
where tm.text_id = textId);
IF FOUND THEN
IF curr_rec.text_id != curr_rec.new_text_id THEN
temp_rec := getTextEditRecord(curr_rec.new_text_id);
--RETURN TABLE HERE
ELSE
-- No Recursive call directly return table
--RETURN TABLE HERE
END IF;
END IF;
--RETURN TABLE HERE
END;
$$ Language plpgsql;
Now I tried searching on google but could find that how to typecast record type to table type.
As in convert temp_rec to table type and return.
Doing this via a recursive function is the most inefficient way of doing it.
But anyhow, you can do something like this:
create or replace function get_text_edit_record(p_text_id integer)
RETURNS Table (
text_id integer,
text_details varchar,
new_text_id integer)
AS $$
DECLARE
curr_rec record;
BEGIN
select tm.text_id, tm.text_details, tm.new_text_id
into curr_rec
from text_master tm
where tm.text_id = p_text_id;
IF FOUND THEN
return query
select curr_rec.text_id, curr_rec.text_details, curr_rec.new_text_id;
--- IS DISTINCT FROM properly deals with NULL values
IF curr_rec.text_id IS DISTINCT FROM curr_rec.new_text_id THEN
return query
select *
from get_text_edit_record(curr_rec.new_text_id);
END IF;
END IF;
END;
$$ Language plpgsql;

Function dosn't return value from insert

I have this code:
CREATE OR REPLACE FUNCTION get_create_tagId(tagName text) RETURNS text AS $$
BEGIN
IF EXISTS(
SELECT * FROM tags WHERE tag = tagName)
THEN
RETURN(SELECT id FROM tags WHERE tag=tagName);
ELSE
INSERT INTO tags (tag) VALUES(tagName) RETURNING id;
END IF ;
END
$$ LANGUAGE plpgsql;
But when i run it i get
ERROR: query has no destination for result data
even though i have RETURNING id
What should i do/change?
You need to store the returned ID into a variable, then return that variable in the ELSE branch:
CREATE OR REPLACE FUNCTION get_create_tagid(p_tagnametext)
RETURNS text AS $$
DECLARE
l_id integer;
BEGIN
IF EXISTS(SELECT * FROM tags WHERE tag = p_tagname)
THEN
RETURN (SELECT id FROM tags WHERE tag = p_tagname);
ELSE
INSERT INTO tags (tag) VALUES(p_tagname)
RETURNING id
INTO l_id;
return l_id;
END IF ;
END
$$
LANGUAGE plpgsql;
If you have a unique index (or constraint) on the tag column (which you really should have), then you can simplify this:
with new_tag as (
insert into tags (tag)
values ('one')
on conflict do nothing
returning id
)
select id
from new_tag
union all
select id
from tags
where tag = 'one';
The insert won't return anything if the tag exists and thus the final select * from new_tag won't return a row, but second part of the union will. If the row did not exists, the final select from tags wouldn't see it and return no row.
This will be more efficient and safe from race conditions.
Of course you can put that into a function too:
CREATE OR REPLACE FUNCTION get_create_tagid(p_tagname text)
RETURNS text AS $$
$$
with new_tag as (
insert into tags (tag)
values (p_tagname)
on conflict do nothing
returning id
)
select id
from new_tag
union all
select id
from tags
where tag = p_tagname;
$$
LANGUAGE sql;
You can declare new variable and get tag_id into it and return. I supposed that id is autoincrement or serial data type.
CREATE OR REPLACE FUNCTION get_create_tagId(tagName text)
RETURNS text AS $$
DECLARE
tag_id INTEGER;
BEGIN
IF EXISTS(SELECT * FROM tags WHERE tag = tagName) THEN
tag_id := (SELECT id FROM tags WHERE tag=tagName);
RETURN tag_id;
ELSE
INSERT INTO tags(tag) VALUES(tagName);
tag_id := (SELECT max(id) FROM tags);
RETURN tag_id;
END IF;
END
$$ LANGUAGE plpgsql;

A column definition list is required for functions returning "record" in Postgresql

In the Below Postgresql Function i am trying to get results from 2 different tables but it throws error ERROR: 42601: a column definition list is required for functions returning "record".Can anyone please help me.
CREATE OR REPLACE FUNCTION load_page_record(IN _session INT) RETURNS RECORD AS
$$
DECLARE r1 RECORD;
DECLARE r2 RECORD;
DECLARE RESULT RECORD;
BEGIN
SELECT array_agg(sq.*) AS arr INTO r1
FROM (SELECT user_id, user_name
FROM "user"
) sq;
SELECT array_agg(sq.*) AS arr INTO r2
FROM (SELECT client_id, client_name
FROM "clients"
) sq;
SELECT r1.arr, r2.arr INTO RESULT;
RETURN RESULT;
END;
$$ LANGUAGE plpgsql;
It returns a record,
so you should call the function as below,
select load_page_record(5);
The error come if you call it as a table
select * from load_page_record(5);
If you want to return a table place you query with join inside the body as follows,
CREATE OR REPLACE FUNCTION load_page_record1(IN _session INT)
RETURNS TABLE (column1 integer, column2 integer) as
$BODY$
SELECT column1, column2
FROM
table1 a
join
table2 b
ON a.id = b.id
$BODY$
LANGUAGE plpgsql;
try this, procedur return table
CREATE OR REPLACE FUNCTION load_page_record(IN _session INT)
RETURNS table(col1 record[],col2 record[]) AS
$BODY$
BEGIN
RETURN QUERY
select
(SELECT array_agg(sq.*)
FROM (SELECT user_id, user_name
FROM "user"
) sq
),
(SELECT array_agg(sq.*)
FROM (SELECT client_id, client_name
FROM "clients"
) sq
);
END;
$BODY$ LANGUAGE plpgsql stable;
edit: convert to text, try it
CREATE OR REPLACE FUNCTION load_page_record(IN _session INT)
RETURNS table(col1 text,col2 text) AS
$BODY$
BEGIN
RETURN QUERY
select
(SELECT array_agg(sq.*)
FROM (SELECT user_id, user_name
FROM "user"
) sq
)::text,
(SELECT array_agg(sq.*)
FROM (SELECT client_id, client_name
FROM "clients"
) sq
)::text;
END;
$BODY$ LANGUAGE plpgsql stable;
try with text:
CREATE OR REPLACE FUNCTION load_page_record(IN _session INT) RETURNS text AS
$$
DECLARE r1 RECORD;
DECLARE r2 RECORD;
DECLARE RESULT text;
BEGIN
SELECT array_agg(sq.*) AS arr INTO r1
FROM (SELECT 'fdfdfd','fdfdd'
) sq;
SELECT array_agg(sq.*) AS arr INTO r2
FROM (SELECT 'dsds','sdsd'
) sq;
SELECT r1.arr, r2.arr INTO RESULT;
RETURN RESULT;
END;
$$ LANGUAGE plpgsql;
and then simply:
select * from load_page_record(8);
but I hope you are aware of the fact that this instruction SELECT r1.arr, r2.arr INTO RESULT; will only assign the first column to RESULT?

Error column reference is ambiguous

I have a function which:
compare two columns from different tables
make the insert
How can I get some improvement on this code because I get this error:
ERROR: column reference "fld_id" is ambiguous
SQL state: 42702
line 17 at PERFORM
CREATE OR REPLACE FUNCTION "SA_PRJ".usp_add_timesheet_test(p_uid integer, p_project_id integer, p_allocated_time numeric, p_achieved_time numeric, p_task_desc character varying, p_obs character varying, p_date timestamp without time zone)
RETURNS void AS
$BODY$
BEGIN
DECLARE sum_alloc_time numeric;
DECLARE alloc_hours integer;
DECLARE fld_id integer;
SELECT #sum_alloc_time = SUM(fld_allocated_time)
from "SD_PRJ".tbl_project_timesheet
where fld_project_id = p_project_id;
SELECT #alloc_hours = p.fld_allocated_days, #fld_id = p.fld_id
FROM "SD_PRJ".tbl_project p
INNER JOIN "SD_PRJ".tbl_project_timesheet t
ON p.fld_id=t.fld_id
where t.fld_project_id = p_project_id;
IF #sum_alloc_time <= #alloc_hours THEN
INSERT INTO "SD_PRJ".tbl_project_timesheet(fld_emp_id, fld_project_id, fld_is_allocated,fld_allocated_time, fld_achieved_time, fld_task_desc, fld_obs, fld_date)
VALUES (p_uid,p_project_id,coalesce(alloc_id,0), p_allocated_time, p_achieved_time,p_task_desc, p_obs, p_date);
RAISE NOTICE 'INSERT OK!';
ELSE
RAISE NOTICE 'NOT OK';
END IF;
END;
$BODY$ LANGUAGE plpgsql;
There are two tables:
"SD_PRJ".tbl_project (
fld_id
,fld_allocated_days)
"SD_PRJ".tbl_project_timesheet (
fld_id
, fld_project_id
,fld_allocated_time)
Working version:
CREATE OR REPLACE FUNCTION "SA_PRJ".usp_add_timesheet_test(
p_uid integer, p_project_id integer, p_allocated_time numeric
, p_achieved_time numeric, p_task_desc varchar, p_obs varchar, p_date timestamp)
RETURNS void AS
$func$
DECLARE
_sum_alloc_time numeric;
_alloc_hours integer;
_fld_id integer;
BEGIN
SELECT SUM(fld_allocated_time)
INTO _sum_alloc_time
FROM "SD_PRJ".tbl_project_timesheet
WHERE fld_project_id = p_project_id;
SELECT p.fld_allocated_days, p.fld_id
INTO _alloc_hours, _fld_id
FROM "SD_PRJ".tbl_project p
JOIN "SD_PRJ".tbl_project_timesheet t USING (fld_id)
WHERE t.fld_project_id = p_project_id;
IF _sum_alloc_time <= _alloc_hours THEN
INSERT INTO "SD_PRJ".tbl_project_timesheet
(fld_emp_id, fld_project_id, fld_is_allocated, fld_allocated_time
, fld_achieved_time, fld_task_desc, fld_obs, fld_date)
VALUES (p_uid, p_project_id, coalesce(alloc_id,0), p_allocated_time
, p_achieved_time, p_task_desc, p_obs, p_date);
-- alloc_id is undefined, you probably need to use SELECT .. FROM .. instead
RAISE NOTICE 'INSERT OK!';
ELSE
RAISE NOTICE 'NOT OK';
END IF;
END
$func$ LANGUAGE plpgsql;
But this is still needlessly inefficient. Could be done in a single DML statement with data-modifying CTEs. Try a search for related questions.
You need to study the basics first.

Return row from upsert method

I have an upsert function that I modified from the documentation. However I have been trying to return the updated or inserted row. I'm calling this function from a node application and I need to keep track of which record has either been updated or inserted especially during sync script.
Here is the function:
create or replace function upsert_test(d TEXT, sys INT, val INT, p INT, inter BOOLEAN)
returns void as $$
begin
update test_table set description=d, code=sys, val_id=val, provider_id=p, connect=inter where code=sys and val_id=val and provider_id=p;
if found then
return;
end if;
begin
insert into test_table (description, code, val_id, provider_id, connect) values (d, sys, val, p, inter);
return;
exception when unique_violation then
end;
return;
end;
$$ language plpgsql;
I have tried to change the return type and have the function return a record but I can't seem to get it working.
Use the RETURNING clause. You can combine it with RETURN QUERY ...
CREATE OR REPLACE FUNCTION upsert_t2(d text, sys int, val int, p int, inter bool)
RETURNS SETOF test_table AS
$func$
BEGIN
RETURN QUERY
UPDATE test_table t
SET description = d
,code = sys
,val_id = val
,provider_id = p
,connect = inter
WHERE t.code = sys
AND t.val_id = val
AND t.provider_id = p
RETURNING t.*;
IF FOUND THEN
RETURN;
END IF;
BEGIN
RETURN QUERY
INSERT INTO test_table (description, code, val_id, provider_id, connect)
VALUES (d, sys, val, p, inter)
RETURNING *;
EXCEPTION WHEN UNIQUE_VIOLATION THEN
END;
RETURN;
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM upsert_t2(...)
Reply to comment
I would try to avoid updates completely that do not change anything. Also, I would look to a data-modifying CTE in a loop:
CREATE OR REPLACE FUNCTION upsert_cte(d text, sys int, val int, p int
, inter bool)
RETURNS SETOF test_table AS
$func$
BEGIN
LOOP
BEGIN
RETURN QUERY
WITH sel AS (
SELECT t.pk_col -- primary key column
FROM test_table t
WHERE t.code = sys
AND t.val_id = val
AND t.provider_id = p
FOR SHARE -- lock
)
, ins AS (
INSERT INTO test_table (description, code, val_id, provider_id, connect)
SELECT d, sys, val, p, inter
WHERE NOT EXISTS (SELECT 1 FROM sel) -- if not found
RETURNING *
)
, upd AS (
UPDATE test_table t
SET description = d
,code = sys
,val_id = val
,provider_id = p
,connect = inter
FROM sel
WHERE sel.pk_col = t.pk_col -- if found (possibly mult. rows)
AND t.description IS DISTINCT FROM d
,t.code IS DISTINCT FROM sys
,t.val_id IS DISTINCT FROM val
,t.provider_id IS DISTINCT FROM p
,t.connect IS DISTINCT FROM inter -- only if anything changes
RETURNING t.*
)
SELECT * FROM ins
UNION ALL
SELECT * FROM upd;
RETURN; -- No error occurred, exit loop
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- inserted in concurrent session
RAISE NOTICE 'It happened!'; -- hardly ever happens, keep looping
END;
END LOOP;
END
$func$ LANGUAGE plpgsql;
Explanation and links in this related answer:
Is SELECT or INSERT in a function prone to race conditions?