Getting an array return on function - sql

I would like to have this function return as an array which contains all ID (integer) from this query, but i am stuck here:
CREATE OR REPLACE FUNCTION public.all_id(
prm_id integer)
RETURNS SETOF integer
LANGUAGE 'plpgsql'
COST 100.0
AS $function$
DECLARE
all_id integer;
-- all_id integer[]; gives an error, while integer only returns last value
BEGIN
SELECT id
COLLECT INTO all_id
FROM subject_data
WHERE sab_subject = (
SELECT sab_subject
FROM subject_data
WHERE id = prm_id
);
RETURN NEXT all_id;
END;
$function$;
SELECT * FROM public.all_id(1);

here's an example of fn:
t=# create or replace function ar() returns int[] as $$
declare ia int[];
begin
select array_agg(oid::int) into ia from pg_database;
return ia;
end;
$$ language plpgsql;
CREATE FUNCTION
t=# select * from ar();
ar
-----------------------------------------------------------
{13505,16384,1,13504,16419,16816,17135,25542,25679,25723}
(1 row)
SETOF would return "table", language is not literal and thus does not require single quotes, and so on...
as a_horse_with_no_name correctly brings out, you don't need plpgsql for this. A simple query will work:
SELECT array_agg(id)
FROM subject_data
WHERE sab_subject = (
SELECT sab_subject
FROM subject_data
WHERE id = PRM_ID_VALUE
)

Related

Return a table that already exist in postgresql function

I have a bunch of functions that return the same table schema, so I have to repeat the same table schema over and over between those functions' declarations, to make this example simple let's say we have two functions that return the same table schema:
Table: people
CREATE TABLE people(full_name TEXT, age integer);
Functions:
CREATE OR REPLACE FUNCTION get_people_by_age(_age integer)
RETURNS TABLE(full_name TEXT, age integer)
LANGUAGE PLPGSQL
AS
$$
BEGIN
RETURN QUERY SELECT * FROM people WHERE people.age = $1;
END
$$
CREATE OR REPLACE FUNCTION get_people_by_name(_full_name text)
RETURNS TABLE(full_name TEXT, age integer)
LANGUAGE PLPGSQL
AS
$$
BEGIN
RETURN QUERY SELECT * FROM people WHERE people.full_name = $1;
END
$$
Is there a way to refer to the existing table within the function declarations? I imagine something like this:
CREATE OR REPLACE FUNCTION get_people_by_age(_age integer)
RETURNS TABLE(people)
LANGUAGE PLPGSQL
AS
$$
BEGIN
RETURN QUERY SELECT * FROM people WHERE people.age = $1;
END
$$
CREATE OR REPLACE FUNCTION get_people_by_name(_full_name text)
RETURNS TABLE(people)
LANGUAGE PLPGSQL
AS
$$
BEGIN
RETURN QUERY SELECT * FROM people WHERE people.full_name = $1;
END
$$
Where instead of declaring the same schema in every function I refer to a table that already exists, is it possible?
Use returns setof
CREATE OR REPLACE FUNCTION get_people_by_name(_full_name text)
RETURNS setof people
LANGUAGE PLPGSQL
AS
$$
BEGIN
RETURN QUERY SELECT * FROM people WHERE people.full_name = _full_name;
END
$$;
Or a bit simpler as a SQL function:
CREATE OR REPLACE FUNCTION get_people_by_name(_full_name text)
RETURNS setof people
LANGUAGE sql
AS
$$
SELECT * FROM people WHERE people.full_name = _full_name;
$$;

Multiple ALTER TABLE ADD COLUMN in one SQL function call

I came across some weird behaviour I'd like to understand.
I create a plpgsql function doing nothing except of ALTER TABLE ADD COLUMN. I call it 2 times on the same table:
A) In a single SELECT sentence
B) In a SQL function with same SELECT as in A)
Results are different: A) creates two columns, while B) creates only one column. Why?
Code:
CREATE FUNCTION add_text_column(table_name text, column_name text) RETURNS VOID
LANGUAGE plpgsql
AS $fff$
BEGIN
EXECUTE '
ALTER TABLE ' || table_name || '
ADD COLUMN ' || column_name || ' text;
';
END;
$fff$
;
-- this function is called only in B
CREATE FUNCTION add_many_text_columns(table_name text) RETURNS VOID
LANGUAGE SQL
AS $fff$
WITH
col_names (col_name) AS (
VALUES
( 'col_1' ),
( 'col_2' )
)
SELECT add_text_column(table_name, col_name)
FROM col_names
;
$fff$
;
-- A)
CREATE TABLE a (id integer);
WITH
col_names (col_name) AS (
VALUES
( 'col_1' ),
( 'col_2' )
)
SELECT add_text_column('a', col_name)
FROM col_names
;
SELECT * FROM a;
-- B)
CREATE TABLE b (id integer);
SELECT add_many_text_columns('b');
SELECT * FROM b;
Result:
CREATE FUNCTION
CREATE FUNCTION
CREATE TABLE
add_text_column
-----------------
(2 rows)
id | col_1 | col_2
----+-------+-------
(0 rows)
CREATE TABLE
add_many_text_columns
-----------------------
(1 row)
id | col_1
----+-------
(0 rows)
I'm using PostgreSQL 10.4. Please note that this is only a minimal working example, not the full functionality I need.
CREATE OR REPLACE FUNCTION g(i INTEGER)
RETURNS VOID AS $$
BEGIN
RAISE NOTICE 'g called with %', i;
END
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS $$
SELECT g(id)
FROM generate_series(1, i) id;
$$ LANGUAGE SQL;
What do you think happens when I run SELECT t(4)? The only statement printed from g() is g called with 1.
The reason for this is your add_many_text_columns function returns a single result (void). Because it's SQL and is simply returning the result of a SELECT statement, it seems to stop executing after getting the first result, which makes sense if you think of it - it can only return one result after all.
Now change the function to:
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS SETOF VOID AS $$
SELECT g(id)
FROM generate_series(1, i) id;
$$ LANGUAGE SQL;
And run SELECT t(4) again, and now this is printed:
g called with 1
g called with 2
g called with 3
g called with 4
Because the function now returns SETOF VOID, it doesn't stop after the first result and executes it fully.
So back to your functions, you could change your SQL function to return SETOF VOID, but it doesn't really make much sense - better I think to change it to plpgsql and have it do a PERFORM:
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS $$
BEGIN
PERFORM g(id)
FROM generate_series(1, i) id;
END
$$ LANGUAGE plpgsql;
That will execute the statement fully and it still returns a single VOID.
eurotrash provided a good explanation.
Alternative solution 1
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS
$func$
SELECT g(id)
FROM generate_series(1, i) id;
SELECT null::void;
$func$ LANGUAGE sql;
Because, quoting the manual:
SQL functions execute an arbitrary list of SQL statements, returning
the result of the last query in the list. In the simple (non-set)
case, the first row of the last query's result will be returned.
By adding a dummy SELECT at the end we avoid that Postgres stops after processing the the first row of the query with multiple rows.
Alternative solution 2
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS
$func$
SELECT count(g(id))
FROM generate_series(1, i) id;
$func$ LANGUAGE sql;
By using an aggregate function, all underlying rows are processed in any case. The function returns bigint (that's what count() returns), so we get the number of rows as result.
Alternative solution 3
If you need to return void for some unknown reason, you can cast:
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS
$func$
SELECT count(g(id))::text::void
FROM generate_series(1, i) id;
$func$ LANGUAGE sql;
The cast to text is a stepping stone because the cast from bigint to void is not defined.

Given two arrays how to get items that are not in both?

I have two arrays in postgresql:
CREATE OR REPLACE FUNCTION func()
RETURNS void AS
$BODY$
declare
first integer[];
second integer[];
array_vb integer[];
array_vb2 integer[];
begin
code....
select array_agg(id) into first
from a
where id = any (array_vb);
select array_agg(id) into second
from a
where id = any (array_vb2);
end;
$BODY$
LANGUAGE plpgsql VOLATILE
I would like to add a raise notice that will print all items that are in first but not in second
for example:
first = [1,10,15,3,7]
second = [1,3,15,4]
it will print 10,7
How do I do that?
You can use PostgreSQL unsent function:
SELECT ARRAY(SELECT unnest(first) EXCEPT SELECT unnest(second))
Example:
SELECT ARRAY(SELECT unnest(ARRAY[1,10,15,3,7]) EXCEPT SELECT unnest(array[1,3,15,4]))
Gives:
array
--------
{10,7}
See SQLFiddle here.
You can use intarray extension:
create extension if not exists intarray;
select array[1,10,15,3,7] - array[1,3,15,4] as result;
result
--------
{7,10}
(1 row)
create or replace function f(a1 int[], a2 int[])
returns void as $body$
begin
raise notice '%', (
select string_agg(i::text, ',')
from
unnest (a1) a1(i)
left join
unnest (a2) a2(i) using (i)
where a2.i is null
);
end;
$body$ language plpgsql volatile
;
select f(array [1,10,15,3,7], array [1,3,15,4]);
NOTICE: 10,7
f
---
(1 row)
Notice that the above function, as it is, can be marked immutable
You can create a function that returns the desired array result:
create or replace function array_except(a1 int[], a2 int[])
returns int[]
as
$$
select array_agg(i)
from (
select i
from unnest(a1) i
except
select a2
from unnest(a2) a2
) t;
$$
language sql;
The function returns an array that contains the elements that are in the first, but not in the second array.
You can use the above function in your actual function:
create or replace function foo()
returns void
as
$BODY$
declare
_first integer[];
_second integer[];
begin
_first := array[1,10,15,3,7];
_second := array[1,3,15,4];
raise notice 'Result %', array_except(_first, _second);
end;
$BODY$
LANGUAGE plpgsql;

How to declare returning type for function, returning unnamed table

I want to create a function, that returns a table with 2 columns:
i integer -- or bigint?
arr integer[] -- array of integer
What should I write instead of ??? in this function:
CREATE OR REPLACE FUNCTION test()
RETURNS ???
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY (
SELECT i, (ARRAY[11,22,33])[i]
FROM generate_series(
1,
array_upper(ARRAY[11,22,33],1)
) i
);
END;
$$;
see the answer for this question,
example of function return table:
CREATE OR REPLACE FUNCTION foo(a int)
RETURNS TABLE(b int, c int) AS $$
BEGIN
RETURN QUERY SELECT i, i+1 FROM generate_series(1, a) g(i);
END;
$$ LANGUAGE plpgsql;
You don't need plpgsql for this. A simple SQL function would do the job:
CREATE OR REPLACE FUNCTION test(_arr anyarray)
RETURNS TABLE (idx int, elem anyelement)
$func$
SELECT i, _arr[i] FROM generate_subscripts(_arr, 1) i
$func$ LANGUAGE sql AS
Call:
SELECT * FROM test(ARRAY[11,22,33]::int[]); -- Cast to declare type for literals
The polymorphic parameter anyarray works for arrays of any base type.
How to write a function that returns text or integer values?
Use generate_subscripts() to simplify the task.
More about returning from a function:
How to return result of a SELECT inside a function in PostgreSQL?
Postgres 9.4
There is a shiny new trick in upcoming Postgres 9.4: WITH ORDINALITY. Details in this related answer:
PostgreSQL unnest() with element number
Simplify to (no additional function):
SELECT * FROM unnest(ARRAY[11,22,33]::int[]) WITH ORDINALITY AS x (elem, idx)

Insert using a function that returns two values per row

This function:
CREATE OR REPLACE FUNCTION fn_test1()
RETURNS SETOF date AS
$BODY$
declare
i int;
begin
i:=0;
while i<5 loop
return next '2001-01-02'::date;
i:=i+1;
end loop;
end
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100
ROWS 1000;
This table:
CREATE TABLE teste1
(
teste1_id serial NOT NULL,
num integer,
fn_date date)
An INSERT like this works just fine (inserting 5 rows):
Insert into teste1(num,fn_date)
select 1, fn_test1();
But if I want to have a function that returns two dates in a row, and a table that has 2 columns of dates how should I do that? I've made this so far:
CREATE OR REPLACE FUNCTION fn_test2()
RETURNS TABLE(a date, b date) AS
$BODY$
declare
_start_date date;
_end_date date;
begin
_start_date:='2001-01-01'::date;
_end_date:='2002-01-01'::date;
i:=0;
while i < 5 loop
return query(select _start_date,_end_date);
i:=i+1;
end loop;
end
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100
ROWS 1000;
And this table:
CREATE TABLE teste2
(
teste2_id serial NOT NULL,
num integer,
start_date date,
end_date date)
Now, I can't do this:
INSERT INTO teste2(num,start_date,end_date)
SELECT 1, fn_test2();
I've made the function return setof mytype (creating a type with two dates) but it seems to do the same thing.
How should I modify the INSERT query or the function to make this work?
To access fields of a (well known) composite type, you need to wrap the identifier in parentheses. Without parenthesis the identifier before the dot would be taken to be a table name per SQL syntax rules. This would work:
SELECT 1, (fn_test2()).*
BTW, your dummy function could be simpler:
CREATE OR REPLACE FUNCTION fn_test2()
RETURNS TABLE(a date, b date) AS
$func$
BEGIN
a := '2001-01-01'::date;
b := '2002-01-01'::date;
FOR i in 0 .. 4 LOOP
RETURN NEXT;
END LOOP;
END
$func$ LANGUAGE plpgsql;
Or use this simple SQL statement with generate_series() to the same effect:
SELECT 1, '2001-01-01'::date AS a, '2002-01-01'::date AS b
FROM generate_series(0,4);
Try using:
INSERT INTO teste2(num,start_date,end_date)
SELECT 1, f.a, f.b FROM fn_test2() AS f;
since you've declared a and b as columns of the table being returned.