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;
Related
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.
I am trying to create procedure which selects data, processes and returns them, but I am struggling how to define array variable for multiple columns.
This works:
CREATE OR REPLACE FUNCTION testing_array_return()
RETURNS TABLE(id BIGINT) AS
$body$
DECLARE
l_rows BIGINT[];
BEGIN
-- select data using for update etc
l_rows := ARRAY(
SELECT 1 AS id
UNION
SELECT 2 AS id
);
-- do some stuff
-- return previously selected data
RETURN QUERY
SELECT *
FROM UNNEST(l_rows);
END;
$body$
LANGUAGE 'plpgsql'
But I want to do this for 2 or more columns without using composite type or rowtype:
CREATE OR REPLACE FUNCTION testing_array_return()
RETURNS TABLE(id BIGINT, text VARCHAR2) AS
$body$
DECLARE
l_rows -- what should I put here?
BEGIN
-- select data using for update etc
l_rows := ARRAY(
SELECT 1 AS id, 'test' AS text
UNION
SELECT 2 AS id, 'test2' AS text
);
-- do some stuff
-- return previously selected data
RETURN QUERY
SELECT *
FROM UNNEST(l_rows);
END;
$body$
LANGUAGE 'plpgsql'
In oracle I could define record type and then table type of it, but I can't find how to do this in postgres. Maybe using wrong keywords when searching...
edit: this is how I would do this in Oracle (without returning).
DECLARE
TYPE t_row IS RECORD(
id NUMBER
,text VARCHAR2(10));
TYPE t_tbl IS TABLE OF t_row;
l_rows t_tbl := t_tbl(); --how to do this in postgres?
BEGIN
SELECT *
BULK COLLECT
INTO l_rows
FROM (SELECT 1 AS id
,'test' AS text
FROM dual
UNION ALL
SELECT 2 AS id
,'test' AS text
FROM dual);
END;
Anything similiar in postgres? Like record but for array.
You would do the same in Postgres:
Create the record type:
create type footype as (id bigint, text_ varchar);
Then use an array of that type in your function:
CREATE OR REPLACE FUNCTION testing_array_return()
RETURNS TABLE(id BIGINT, text VARCHAR) AS
$body$
DECLARE
l_rows footype[];
BEGIN
-- select data using for update etc
l_rows := ARRAY(
SELECT (1, 'test')
UNION
SELECT (2, 'test2')
);
-- do some stuff
-- return previously selected data
RETURN QUERY
SELECT *
FROM UNNEST(l_rows);
END;
$body$
LANGUAGE plpgsql;
Online example: http://rextester.com/UMRZFO54266
The expression (1, 'test') creates a single value of record type. As that is assigned to a typed variable, there is no need to alias the columns (and in fact you can't do that anyway).
Unrelated, but: the language name is an identifier. Do not put that in single quotes.
Note that text is also a keyword in Postgres because it's a data type. You shouldn't use it as a column name
New to Stored Procedures , have a requirement where I need to execute multiple queries inside stored procedure and return results. I would like to know whether this is possible or not ..
Ex :
Query 1 returns a list of userid ..
Select userid from user where username = ?
For each userid from the above query , I need to execute three different queries like
Query 2 select session_details from session where userid = ?
Query 3 select location from location where userid = ?
The return value should be a collection of , session_details and location.
Is this possible,can you provide some hints?
You can loop through query results like so:
FOR id IN Select userid from user where username = ?
LOOP
...
END LOOP;
As #Fahad Anjum says in his comment, its better if you can do it in a query. But if that's not posible, you have tree posibilities to achive what you want.
SETOF
TABLE
refcursor
1. SETOF
You can return a set of values. The set can be an existing table, a temporal table, or a TYPE you define.
TYPE example:
-- In your case the type could be (userid integer, session integer, location text)
CREATE TYPE tester AS (id integer);
-- The pl returns a SETOF the created type.
CREATE OR REPLACE FUNCTION test() RETURNS SETOF tester
AS $$
BEGIN
RETURN QUERY SELECT generate_series(1, 3) as id;
END;
$$ LANGUAGE plpgsql
-- Then, you get the set by selecting the PL as if it were a table.
SELECT * FROM test();
Table and Temp Table examples:
-- Create a temporal table o a regular table:
CREATE TEMP TABLE test_table(id integer);
-- or CREATE TABLE test_table(id integer);
-- or use an existing table in your schema(s);
-- The pl returns a SETOF the table you need
CREATE OR REPLACE FUNCTION test() RETURNS SETOF test_table
AS $$
BEGIN
RETURN QUERY SELECT generate_series(1, 3) as id;
END;
$$ LANGUAGE plpgsql
-- Then, you get the set by selecting the PL as if it were a table.
SELECT * FROM test();
-- NOTE: Since you are only returning a SETOF the table,
-- you don't insert any data into the table.
-- So, if you select the 'temp' table you won't see any changes.
SELECT * FROM test_table
2. TABLE
A PL can return a table, it would be similar to create a temporal table and then return a SETOF, but, in this case you declare de 'temp' table on the 'returns' sentence of the PL.
-- Next to TABLE you define the columns of the table the PL will return
CREATE OR REPLACE FUNCTION test() RETURNS TABLE (id integer)
AS $$
BEGIN
RETURN QUERY SELECT generate_series(1, 3) as id;
END;
$$ LANGUAGE plpgsql
-- As the other examples, you select the PL to get the data.
SELECT * FROM test();
3. refcursor
This one is the more complex solution. You return a cursor, not the actual data. If you need 'dynamic' values for your returning set, this is the solution.
But since you need static data, you won't need this option.
The use of any of these ways depends on any specific case, if you use regularly the userid,session,location in different ways and PLs, it would be better to Use the SETOF with a type.
If you have a table that has the userid,session,location columns, it's better to return a SETOF table.
If you just use the userid,session,location for one case, then it would be better to use a 'RETURNS TABLE' approach.
If you need to return a dynamic set you would have to use cursors... but that solution is really more advanced.
Based solely on your example, here's probably the easiest way to do it:
CREATE FUNCTION my_func(user_id INTEGER)
RETURNS TABLE (userid INTEGER, session INTEGER, location TEXT) AS
$$
SELECT u.userid, s.session, l.location
FROM -- etc... your query here
$$
LANGUAGE SQL STABLE;
Addressing comment:
That's a bit of a different question. One question is how to return multiple records containing multiple fields in a stored procedure. One way is as above.
The other question is how to write a query that gets data from multiple tables. Again, there are many ways to do it. One way is (again, based on my interpretation of your requirements in the examples):
SELECT userid
, ARRAY_AGG(SELECT session_details FROM session s WHERE s.userid = u.userid)
, ARRAY_AGG(SELECT l.location FROM location l WHERE l.userid = u.userid)
FROM user u
WHERE username = user_name
This will return one record containing the user_id, an array of session_details for that user, and an array of locations for that user.
Then the function can be changed to:
CREATE FUNCTION my_func(user_name TEXT, OUT userid INTEGER, OUT session_details TEXT[], OUT locations TEXT[])
AS $$
SELECT userid
, ARRAY(SELECT session_details FROM session s WHERE s.userid = u.userid)
, ARRAY(SELECT l.location FROM location l WHERE l.userid = u.userid)
FROM user u
WHERE username = user_name;
$$ LANGUAGE SQL STABLE;
I have plpgsql function:
CREATE OR REPLACE FUNCTION test() RETURNS VOID AS
$$
DECLARE
my_row my_table%ROWTYPE;
BEGIN
SELECT * INTO my_row FROM my_table WHERE id='1';
my_row.date := now();
END;
$$ LANGUAGE plpgsql;
I would like to know if it's possible to directly UPDATE my_row record.
The only way I've found to do it now is:
UPDATE my_table SET date=now() WHERE id='1';
Note this is only an example function, the real one is far more complex than this.
I'm using PostgreSQL 9.2.
UPDATE:
Sorry for the confusion, what I wanted to say is:
SELECT * INTO my_row FROM my_table INTO my_row WHERE id='1';
make_lots_of_complicated_modifications_to(my_row, other_complex_parameters);
UPDATE my_row;
I.e. Use my_row to persist information in the underlying table. I have lots of parameters to update.
I would like to know if it's possible to directly update "my_row"
record.
It is.
You can update columns of a row or record type in plpgsql - just like you have it. It should be working, obviously?
This would update the underlying table, of course, not the variable!
UPDATE my_table SET date=now() WHERE id='1';
You are confusing two things here ...
Answer to clarification in comment
I don't think there is syntax in PostgreSQL that can UPDATE a whole row. You can UPDATE a column list, though. Consider this demo:
Note how I use thedate instead of date as column name, date is a reserved word in every SQL standard and a type name in PostgreSQL.
CREATE TEMP TABLE my_table (id serial, thedate date);
INSERT INTO my_table(thedate) VALUES (now());
CREATE OR REPLACE FUNCTION test_up()
RETURNS void LANGUAGE plpgsql AS
$func$
DECLARE
_r my_table;
BEGIN
SELECT * INTO _r FROM my_table WHERE id = 1;
_r.thedate := now()::date + 5 ;
UPDATE my_table t
-- explicit list of columns to be to updated
SET (id, thedate) = (_r.id, _r.thedate)
WHERE t.id = 1;
END
$func$;
SELECT test_up();
SELECT * FROM my_table;
However, you can INSERT a whole row easily. Just don't supply a column list for the table (which you normally should, but in this case it is perfectly ok, not to).
As an UPDATE is internally a DELETE followed by an INSERT anyway, and a function automatically encapsulates everything in a transaction, I don't see, why you couldn't use this instead:
CREATE OR REPLACE FUNCTION x.test_ delins()
RETURNS void LANGUAGE plpgsql AS
$func$
DECLARE
_r my_table;
BEGIN
SELECT * INTO _r
FROM my_table WHERE id = 1;
_r.thedate := now()::date + 10;
DELETE FROM my_table t WHERE t.id = 1;
INSERT INTO my_table SELECT _r.*;
END
$func$;
I managed to get this working in PLPGSQL in a couple of lines of code.
Given a table called table in a schema called example, and a record of the same type declared as _record, you can update all the columns in the table to match the record using the following hack:
declare _record example.table;
...
-- get the columns in the correct order, as a string
select string_agg(format('%I', column_name), ',' order by ordinal_position)
into _columns
from information_schema.columns
where table_schema='example' and table_name='table';
execute 'update example.table set (' || _columns || ') = row($1.*) where pkey=$2'
using _record, _record.pkey;
In the above example, of course, _record.pkey is the table's primary key.
Postgresql has not set row in update.
If you wont update full row you should assign value for each column separately
yes, its possible to update / append the row-type variable,
CREATE OR REPLACE FUNCTION test() RETURNS VOID AS $$
DECLARE
my_row my_table%ROWTYPE;
BEGIN
SELECT * INTO my_row FROM my_table WHERE id='1';
my_row.date := now();
raise notice 'date : %; ',my_row.date;
END;
$$ LANGUAGE plpgsql;
here the raise notice will display the today's date only.
but this will not update the column date in my_table.
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);