Infinity recursive function exception in Postgresql - postgresql

I have a table Answer and Many to Many table Link (Answer n-n Answer)
Link have 2 column : from_id and to_id reference to answer_id.
I want get all descendant of answer by answer_id ( from_id in Link ).
I have written function as below :
CREATE OR REPLACE FUNCTION getAllChild(_answer_id BIGINT)
RETURNS SETOF BIGINT AS $$
DECLARE r link;
BEGIN
FOR r IN
SELECT * FROM link
WHERE from_id = _answer_id
LOOP
RETURN NEXT r.to_id;
RETURN QUERY SELECT * FROM getAllChild(r.to_id);
END LOOP;
RETURN;
END;
$$ LANGUAGE plpgsql STRICT;
SELECT * FROM getAllChild(1);
The result is fine if to_id not duplicate with from_id that already got otherwise I will get recursive infinity.
My question is how I can make loop skip the existed to_id to call getAllChild() in RETURN QUERY

I'd suggest you do this with a recursive CTE, you could use the same approach in a function though.
You can use an array to keep a track of all the from_id's you've dealt with, and then in the next run through you ignore any records for from_id's already in the results. In the code below I'm using the path array to track all the from_id's already seen.
with recursive t as
(
select l.from_id,l.to_id, ARRAY[l.from_id] as path, 1 as depth
from link l where from_id = 2
union all
select l.from_id,l.to_id, array_append(t.path,l.from_id), t.depth+1
from link l
inner join t on l.from_id = t.to_id
where not (l.from_id = ANY (t.path)) -- ignore records already processed
)
select * from t;
Fiddle at: http://sqlfiddle.com/#!15/024e80/1
Updated: As a function
CREATE OR REPLACE FUNCTION getAllChild(_answer_id BIGINT)
RETURNS SETOF BIGINT AS $$
BEGIN
return query
with recursive t as
(
select l.from_id,l.to_id, ARRAY[l.from_id] as path, 1 as depth from link l where from_id = _answer_id
union all
select l.from_id,l.to_id, array_append(t.path,l.from_id), t.depth+1 from link l
inner join t on l.from_id = t.to_id
where not (l.from_id = ANY (t.path))
)
select to_id from t;
END;
$$ LANGUAGE plpgsql STRICT;
Arrays documentation: https://www.postgresql.org/docs/current/static/arrays.html
CTEs: https://www.postgresql.org/docs/current/static/queries-with.html

Related

possible to have multiple temp tables per postgres function?

i'm trying to find the total number of contacts who ARE in some lists, but who are NOT in others. basically i want to get the set difference between these two groups of contacts.
here's my function. i'm using supabase. this function works perfectly via the supabase browser sql editor
CREATE or replace FUNCTION get_net_num_contacts(to_list_ids bigint[], not_to_list_ids bigint[])
returns bigint
AS $$
declare
net_num_contacts bigint;
BEGIN
CREATE TEMP TABLE IF NOT EXISTS t1 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(to_list_ids);
CREATE TEMP TABLE IF NOT EXISTS t2 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(not_to_list_ids);
CREATE TEMP TABLE IF NOT EXISTS t3 AS
select t1.contact_id from t1 except select t2.contact_id from t2;
SELECT count(*) from t3 into net_num_contacts;
return net_num_contacts;
END;
$$ LANGUAGE plpgsql;
but when i save this to my db and try running it in my web app with an .rpc call, it always returns 0.
i did some tinkering and found that the function, loaded into supabase via my filesystem (supabase start), started returning empty sets any time i tried to create 2 or more temp tables.
this works great:
CREATE or replace FUNCTION demo_works(to_list_ids bigint[], not_to_list_ids bigint[])
returns setof bigint
AS $$
declare
net_num_contacts bigint;
BEGIN
CREATE TEMP TABLE IF NOT EXISTS t1 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(to_list_ids);
return query select * from t1;
END;
$$ LANGUAGE plpgsql;
doesn't work, always returns an empty array:
CREATE or replace FUNCTION demo_doesnt_work(to_list_ids bigint[], not_to_list_ids bigint[])
returns setof bigint
AS $$
declare
net_num_contacts bigint;
BEGIN
CREATE TEMP TABLE IF NOT EXISTS t1 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(to_list_ids);
CREATE TEMP TABLE IF NOT EXISTS t2 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(not_to_list_ids);
return query select * from t1;
END;
$$ LANGUAGE plpgsql;
i've seen hints online that maybe it's understood that you can't have multiple temp tables in postgres (i lost the link), and indications that some systems dont allow temp tables in functions at all.
so i'm confused as to why this works in the supabase sql editor but not after being saved into my db. and hoping to gain clarity on the limitations here, or maybe i'm doing something irrelevant wrong.
and if anyone has insight on how to properly accomplish what i'm trying to do, that would be great too
update
here's my successful alternative function for getting the set difference between these two groups of contacts, if useful for anyone. and open to feedback. but i'm still very curious about why my initial attempt didn't usually work.
CREATE or replace FUNCTION get_net_num_contacts(to_list_ids bigint[], not_to_list_ids bigint[])
returns bigint
AS $$
declare
net_num_contacts bigint;
BEGIN
WITH r0 as
(
WITH r1 AS
(
select distinct contact_id from list_members where list_id = ANY(to_list_ids)
)
select contact_id from r1 except
select contact_id from list_members where list_id = ANY(not_to_list_ids)
)
select count(*) from r0 into net_num_contacts;
return net_num_contacts;
END;
$$ LANGUAGE plpgsql;

Query on Return Statement - PostgreSQL

I have this question, I was doing some migration from SQL Server to PostgreSQL 12.
The scenario, I am trying to accomplish:
The function should have a RETURN Statement, be it with SETOF 'tableType' or RETURN TABLE ( some number of columns )
The body starts with a count of records, if there is no record found based on input parameters, then simply Return Zero (0), else, return the entire set of record defined in the RETURN Statement.
The Equivalent part in SQL Server or Oracle is: They can just put a SELECT Statement inside a Procedure to accomplish this. But, its a kind of difficult in case of PostgreSQL.
Any suggestion, please.
What I could accomplish still now - If no record found, it will simply return NULL, may be using PERFORM, or may be selecting NULL as column name for the returning tableType columns.
I hope I am clear !
What I want is something like -
============================================================
CREATE OR REPLACE FUNCTION public.get_some_data(
id integer)
RETURNS TABLE ( id_1 integer, name character varying )
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
p_id alias for $1;
v_cnt integer:=0;
BEGIN
SELECT COUNT(1) FROM public.exampleTable e
WHERE id::integer = e.id::integer;
IF v_cnt= 0 THEN
SELECT 0;
ELSE
SELECT
a.id, a.name
public.exampleTable a
where a.id = p_id;
END;
$BODY$;
If you just want to return a set of a single table, using returns setof some_table is indeed the easiest way. The most basic SQL function to do that would be:
create function get_data()
returns setof some_table
as
$$
select *
from some_table;
$$
language sql;
PL/pgSQL isn't really necessary to put a SELECT statement into a function, but if you need to do other things, you need to use RETURN QUERY in a PL/pgSQL function:
create function get_data()
returns setof some_table
as
$$
begin
return query
select *
from some_table;
end;
$$
language plpgsql;
A function as exactly one return type. You can't have a function that sometimes returns an integer and sometimes returns thousands of rows with a dozen columns.
The only thing you could do, if you insist on returning something is something like this:
create function get_data()
returns setof some_table
as
$$
begin
return query
select *
from some_table;
if not found then
return query
select (null::some_table).*;
end if;
end;
$$
language plpgsql;
But I would consider the above an extremely ugly and confusing (not to say stupid) solution. I certainly wouldn't let that pass through a code review.
The caller of the function can test if something was returned in the same way I implemented that ugly hack: check the found variable after using the function.
One more hack to get as close as possible to what you want. But I will repeat what others have told you: You cannot do what you want directly. Just because MS SQL Server lets you get away poor coding does not mean Postgres is obligated to do so. As the link by #a_horse_with_no_name implies converting code is easy, once you migrate how you think about the problem in the first place. The closest you can get is return a tuple with a 0 id. The following is one way.
create or replace function public.get_some_data(
p_id integer)
returns table ( id integer, name character varying )
language plpgsql
as $$
declare
v_at_least_one boolean = false;
v_exp_rec record;
begin
for v_exp_rec in
select a.id, a.name
from public.exampletable a
where a.id = p_id
union all
select 0,null
loop
if v_exp_rec.id::integer > 0
or (v_exp_rec.id::integer = 0 and not v_at_least_one)
then
id = v_exp_rec.id;
name = v_exp_rec.name;
return next;
v_at_least_one = true;
end if;
end loop ;
return;
end
$$;
But that is still just a hack and assumes there in not valid row with id=0. A much better approach would by for the calling routing to check what the function returns (it has to do that in one way or another anyway) and let the function just return the data found instead of making up data. That is that mindset shift. Doing that you can reduce this function to a simple select statement:
create or replace function public.get_some_data2(
p_id integer)
returns table ( id integer, name character varying )
language sql strict
as $$
select a.id, a.name
from public.exampletable a
where a.id = p_id;
$$;
Or one of the other solutions offered.

PostgreSQL: How to use SELECT statement within CASE expression in function?

Can we able to use SELECT statement within CASE conditional expression in function?
I have tried the following function to accomplish the above task.
Example: I have a function which is used to display table containing some rows.
create or replace function test(n integer)
returns table (name text,city text) as
$body$
begin
case n when 1 then
select * from table1
when 2 then
select * from table2
when 3 then
select * from view1
end;
end;
$body$
language plpgsql;
--Calling function
select * from test(1);
/*have to show details of table1*/
select * from test(2);
/*have to show details of table2*/
select * from test(3);
/*have to display details of view1*/
Actually, there is a plpgsql CASE statement, not to be confused with the SQL CASE expression:
CREATE OR REPLACE function test(n integer)
RETURNS TABLE (name text, city text) AS
$func$
BEGIN
CASE n
WHEN 1 THEN
RETURN QUERY SELECT t.name, t.city FROM table1 t;
WHEN 2 THEN
RETURN QUERY SELECT t.foo, t.bar FROM table2 t;
WHEN 3 THEN
RETURN QUERY SELECT t.bar, t.bamm FROM view1 t;
END CASE;
END
$func$ LANGUAGE plpgsql;
If you declare your function as RETURNS TABLE (name text, city text), then your SELECT statements should have a column list with matching types.
If on the other hand you want to SELECT *, declare the function as RETURNS SETOF table1 accordingly.
When naming columns in the return type, those variable are visible in the function body. Be sure to table-qualify column names that would conflict with same names. So t.name instead of just name.
Column names from queries inside the function are not visible outside. Only the declared return type. So names don't have to match, just data types.
Either way, I suggest to simplify things:
CREATE OR REPLACE function test(n integer)
RETURNS SETOF table1 AS
$func$
SELECT * FROM table1 t WHERE n = 1
UNION ALL
SELECT * FROM table2 t WHERE n = 2
UNION ALL
SELECT * FROM view1 t WHERE n = 3;
$func$ LANGUAGE sql;
Same result. Just as fast. SQL or PL/pgSQL function is a matter of taste and some other details. PL/pgSQL is probably faster for stand-alone calls. SQL can more easily be nested.
For this you need an IF statement and RETURN QUERY, e.g.
create or replace function test(n integer)
returns table (name text,city text) as
$body$
begin
IF n = 1 THEN
RETURN QUERY select * from table1;
ELIF n = 2 THEN
RETURN QUERY select * from table2;
ELIF n = 3 THEN
RETURN QUERY select * from view1;
END IF;
end;
$body$
language plpgsql;
It's a very weird thing to want to do, though.

Returning result set from Postgres functions

In my Postgres 9.2 database, I need to build a function that takes several parameters, performs several queries, and then returns a data set that is composed of several rows and several columns. I've built several test functions to get a better grasp of Postgres' functionality, here is one:
CREATE OR REPLACE FUNCTION sql_with_rows11(id integer) RETURNS character varying AS
$BODY$
declare vid integer;
declare vendor character varying;
BEGIN
vid := (select v_id from public.gc_alerts where a_id = id);
vendor := (select v_name from public.gc_vendors where v_id = vid);
RETURN vendor;
END;
$BODY$
LANGUAGE plpgsql;
I know that I can combine this into one query, but this is more of a practice exercise. This works fine and I get the vendor name. However, I need to return more than one column from the gc_vendors table.
Ultimately, I need to return columns from several tables based on subqueries. I've looked into creating a result set function, but I believe it only returns one row at a time. I also looked into returning setof type, but that seems to be limited to existing tables.
After initial feedback, I changed the function to the following:
CREATE OR REPLACE FUNCTION sql_with_rows14(IN v_uid character varying, IN lid integer)
RETURNS table (aid int, aname character varying) AS
$BODY$
declare aid integer;
declare aname character varying;
BEGIN
sql_with_rows14.aid := (select a_id from public.gc_alerts where v_id = sql_with_rows14.v_uid);
sql_with_rows14.aname := (select a_name from public.gc_alerts where a_id = sql_with_rows14.aid);
RETURN;
END;
$BODY$
LANGUAGE plpgsql;
I also tried RETURN NEXT, but same results.
When I query it, if the query returns only one row, it works fine. However it doesn't work for multiple rows. I also tried something like this, with the same result:
...
BEGIN
sql_with_rows14.aid := (select a_id from public.gc_alerts);
sql_with_rows14.aname := (select a_name from public.gc_alerts);
RETURN NEXT;
END;
I need to return more than one column from the gc_vendors table
To return a single row with multiple fields (as opposed to a set of rows), you can either use:
RETURNS row_type
.. where row_type is a pre-defined composite type (like a table name, that serves as such automatically). Or:
RETURNS record
combined with OUT parameters. Be aware that OUT parameters are visible in the body almost everywhere and avoid naming conflicts.
Using the second option, your function could look like this:
CREATE OR REPLACE FUNCTION sql_with_columns(IN _id integer -- IN is optional default
, OUT vid integer
, OUT vendor text)
RETURNS record
LANGUAGE plpgsql AS
$func$
BEGIN
SELECT INTO vid v_id
FROM public.gc_alerts
WHERE a_id = id;
SELECT INTO vendor v_name
FROM public.gc_vendors
WHERE v_id = vid;
RETURN; -- just noise, since OUT parameters are returned automatically
END
$func$;
As you mentioned, you should combine both queries into one, or even use a plain SQL statement instead. This is just a show case. The excellent manual has all the details.
You can also use:
RETURNS TABLE (...)
Or:
RETURNS SETOF row_type
This allows to return a set of rows (0, 1 or many). But that's not in your question.
To get individual columns instead of a record representation, call the function with:
SELECT * FROM sql_with_columns(...);
There are lots of examples here on SO, try a search - maybe with additional key words.
Also read the chapter "Returning from a Function" in the manual.
First of all, consider using views or simple queries. I'd say that if you can process something with a simple query, you shouldn't create function for that. in your case, you can use this query
select
v.v_name, v.* -- or any other columns from gc_alerts or gc_vendors
from public.gc_alerts as a
inner join public.gc_vendors as v on v.v_id = a.vid
where a.a_id = <your id here>
if you want your function to return rows, you can declare it like
CREATE OR REPLACE FUNCTION sql_with_rows11(id integer)
RETURNS table(vendor text, v_id int)
as
$$
select
v.v_name, v.v_id
from public.gc_alerts as a
inner join public.gc_vendors as v on v.v_id = a.vid
where a.a_id = id
$$ language SQL;
or plpgsql function:
CREATE OR REPLACE FUNCTION sql_with_rows11(id integer)
RETURNS table(vendor text, vid int)
AS
$$
declare vid integer;
declare vendor character varying;
BEGIN
sql_with_rows11.vid := 1; -- prefix with function name because otherwise it would be declared variables
sql_with_rows11.vendor := 4;
return next;
sql_with_rows11.vid := 5;
sql_with_rows11.vendor := 8;
return next;
END;
$$ LANGUAGE plpgsql;
sql fiddle demo to fiddle with :)

update many to many records with postgresql / plpgsql

I have a web based system that has several tables (postgres/pgsql) that hold many to many relationships such as;
table x
column_id1 smallint FK
column_id2 smallint FK
In this scenario the update is made based on column_id2
At first to update these records we would run the following function;
-- edited to protect the innocent
CREATE FUNCTION associate_id1_with_id2(integer[], integer) RETURNS integer
AS $_$
DECLARE
a alias for $1;
b alias for $2;
i integer;
BEGIN
delete from tablex where user_id = b;
FOR i IN array_lower(a,1) .. array_upper(a,1) LOOP
INSERT INTO tablex (
column_id2,
column_id1)
VALUES (
b,
a[i]);
end loop;
RETURN i;
END;
$_$
LANGUAGE plpgsql;
that seemed sloppy and now with the addition of auditing it really shows.
What I am trying to do now is only delete and insert the necessary rows.
I have been trying various forms of the following with no luck
CREATE OR REPLACE FUNCTION associate_id1_with_id2(integer[], integer) RETURNS integer
AS $_$
DECLARE
a alias for $1;
b alias for $2;
c varchar;
i integer;
BEGIN
c = array_to_string($1,',');
INSERT INTO tablex (
column_id2,
column_id1)
(
SELECT column_id2, column_id1
FROM tablex
WHERE column_id2 = b
AND column_id1 NOT IN (c)
);
DELETE FROM tablex
WHERE column_id2 = b
AND column_id1 NOT IN (c);
RETURN i;
END;
$_$
LANGUAGE plpgsql;
depending on the version of the function I'm attempting there are various errors such as explicit type casts (i'm guessing it doesnt like c being varchar?) for the current version.
first off, is my approach correct or is there a more elegant solution given there are a couple tables which this type of handling is required? If not could you please point me in the right direction?
if this is the right approach could you please assist with the array conversion for the NOT IN portion of the where clause?
Instead of array_to_string, use unnest to transform the array into a set of rows (as if it was a table), and the problem can be solved with vanilla SQL:
INSERT INTO tablex(column_id1,column_id2)
select ai,b from unnest(a) as ai where not exists
(select 1 from tablex where column_id1=ai and column_id2=b);
DELETE FROM tablex
where column_id2=b and column_id1 not in
(select ai from unnest(a) as ai);