Return array as result from function - sql

I cant solve one issue. Hope that you can help me to find rigth way.
This func works correctly:
CREATE OR REPLACE FUNCTION stress_test_dates(_organization_id int,
OUT date_start int, OUT date_end int) RETURNS record
AS
$$
BEGIN
select (CASE WHEN min(E.ts) IS NULL THEN 0 ELSE int4(date_part('epoch', min(E.ts)::date)) END) as start_date,
(CASE WHEN max(E.ts) IS NULL THEN 0 ELSE int4(date_part('epoch', max(E.ts)::date)) END) as end_date
INTO date_start, date_end
from employee_events E
join employees Y
on Y.id = E.employee_id AND
Y.organization_id = _organization_id
where name = 'survey_end';
END;
$$ language 'plpgsql';
But I am trying to make it to return an array:
CREATE OR REPLACE FUNCTION stress_test_range(_organization_id int,
OUT avaible_dates double precision[] ) RETURNS record
AS
$$
BEGIN
SELECT ( ARRAY (select (date_part('epoch', (E.ts)::date))
from employee_events E
join employees Y
on Y.id = E.employee_id AND
Y.organization_id = 2
where name = 'survey_end'))
;
END;
$$ language 'plpgsql';
Output is here:[enter image description here]
psql:norbu/sql/funcs/stress_test_range.sql:14: ОШИБКА: результат функции должен иметь тип double precision[] (в соответствии с параметрами OUT)
I cont understand, why

Related

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?

Creating Custom columns in stored procedure postgres

I am using a stored procedure to return the type of student that is enrolled at my college. Pushing their ID through should return their first name and last name in a new column that is going to be made(Ex: commuter, employee, resident). I keep getting an error:
ERROR: syntax error at or near "if"
LINE 8: if exists (select count(commuterid) > 0 from commuter wh...).
Any tips or ideas?
create or replace function roleAtMarist(int, REFCURSOR) returns refcursor as
$$
declare
identifier int := $1;
resultset refcursor := $2;
begin
open resultset for
if exists (select count(commuterid) > 0 from commuter where commuterid = identifier) then
select fname, lname, "Commuter" as Role
from people
where peopleid = identifier;
end if;
if exists (select count(employeeid) > 0 from employee where emplpoyeeid = identifier) then
select fname, lname, "Employee" as Role
from people
where peopleid = identifier;
end if;
if exists (select count(residentid) > 0 from studentpark where residentid = identifier) then
select fname, lname, "Resident" as Role
from people
where peopleid = identifier;
end if;
return resultset;
end;
$$
language plpgsql;
select roleAtMarist(12, 'resultset') ;
fetch all from results ;
This is backwards in multiple ways. A cursor would use valid SQL and no plpgsql commands. But you don't need a cursor nor plpgsql to begin with. A simple SQL function should do:
CREATE OR REPLACE FUNCTION role_at_marist(_identifier int)
RETURNS TABLE (fname text, lname text, "role" text) AS
$func$
SELECT p.fname, p.lname, text 'Commuter'
FROM people
WHERE p.peopleid = $1
AND EXISTS (SELECT 1 FROM commuter c WHERE c.commuterid = p.peopleid)
UNION ALL
SELECT p.fname, p.lname, 'Employee'
FROM people
WHERE p.peopleid = $1
AND EXISTS (SELECT 1 FROM employee e WHERE e.emplpoyeeid = p.peopleid)
UNION ALL
SELECT p.fname, p.lname, 'Resident'
FROM people
WHERE p.peopleid = $1
AND EXISTS (SELECT 1 FROM studentpark s WHERE s.residentid = p.peopleid)
$func$ LANGUAGE sql;
Call:
SELECT * FROM role_at_marist(12);
Set-returning functions can be used just like tables in the FROM list.
String literals are enclosed in single quotes!

Passing record into Postgres function

I'm trying to create an aggregate function that takes a set of records, does some calculations, and returns a number.
I have gotten the following dummy example to work:
CREATE OR REPLACE FUNCTION dummy_func(text) RETURNS int
AS
$$
DECLARE
rec record;
num int := 0;
BEGIN
FOR rec IN EXECUTE $1
LOOP
IF POSITION('some text' IN rec.known_column_name) = 1
THEN
num := num + 1;
END IF;
END LOOP;
RETURN num;
END;
$$ LANGUAGE PLPGSQL;
If I enter
SELECT dummy_func('select * from some_table');
, it works as expected.
However, I would like to make it so that I can use multiple functions and conditions, such as:
SELECT conditional_col_1, dummy_func_1(*), dummy_func_2(*)
FROM some_table
WHERE conditional_col_2 = 'some val'
GROUP BY 1
How can I rewrite my dummy example into being able to function as so (or use a bunch of UNIONS ALL, etc)?
In response to Hambone
If I have the following logic (counting 'a' or counting 'b' * column something3)
CREATE OR REPLACE FUNCTION count_a(text) RETURNS int
AS
$$
DECLARE
rec record;
num int := 0;
BEGIN
FOR rec IN EXECUTE $1
LOOP
num := num + length(regexp_replace(rec.something1, '[^a]+', '', 'g'));
END LOOP;
RETURN num;
END;
$$ LANGUAGE PLPGSQL;
CREATE OR REPLACE FUNCTION count_e_times_something3(text) RETURNS int
AS
$$
DECLARE
rec record;
num int := 0;
BEGIN
FOR rec IN EXECUTE $1
LOOP
num := num + length(regexp_replace(rec.something1, '[^e]+', '', 'g')) * rec.something3::int;
END LOOP;
RETURN num;
END;
$$ LANGUAGE PLPGSQL;
I currently have to execute them individually,
$=# SELECT count_a('SELECT * FROM MY_TEST WHERE something2 = ''A'' and something1 = ''0''');
-[ RECORD 1 ]
count_a | 0
$=# SELECT count_a('SELECT * FROM MY_TEST WHERE something2 = ''A'' and something3 = ''0''');
-[ RECORD 1 ]
count_a | 2
$=# SELECT count_e_times_something3('SELECT * FROM MY_TEST WHERE something2 = ''A'' and something3 = ''0''');
-[ RECORD 1 ]------------+--
count_e_times_something3 | 0
$=# SELECT count_e_times_something3('SELECT * FROM MY_TEST WHERE something2 = ''A'' and something3 = ''1''');
-[ RECORD 1 ]------------+--
count_e_times_something3 | 7
It would be a lot nicer if I could use the following query (I know I need to define an Aggregate function but I can't find any examples where records are taken in an scalars are returned):
SELECT something2, count_a(*), count_e_times_something3(*) FROM MY_TEST WHERE something3 = '1';
something: 'A'
count_a: 3
count_e_times_something3: 7
something: 'B'
count_a: 3
count_e_times_something3: 2
Both count_a resulting in 3 is a coincidence

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?

Return multiple fields as a record in PostgreSQL with PL/pgSQL

I am writing a SP, using PL/pgSQL.
I want to return a record, comprised of fields from several different tables. Could look something like this:
CREATE OR REPLACE FUNCTION get_object_fields(name text)
RETURNS RECORD AS $$
BEGIN
-- fetch fields f1, f2 and f3 from table t1
-- fetch fields f4, f5 from table t2
-- fetch fields f6, f7 and f8 from table t3
-- return fields f1 ... f8 as a record
END
$$ language plpgsql;
How may I return the fields from different tables as fields in a single record?
[Edit]
I have realized that the example I gave above was slightly too simplistic. Some of the fields I need to be retrieving, will be saved as separate rows in the database table being queried, but I want to return them in the 'flattened' record structure.
The code below should help illustrate further:
CREATE TABLE user (id int, school_id int, name varchar(32));
CREATE TYPE my_type AS (
user1_id int,
user1_name varchar(32),
user2_id int,
user2_name varchar(32)
);
CREATE OR REPLACE FUNCTION get_two_users_from_school(schoolid int)
RETURNS my_type AS $$
DECLARE
result my_type;
temp_result user;
BEGIN
-- for purpose of this question assume 2 rows returned
SELECT id, name INTO temp_result FROM user where school_id = schoolid LIMIT 2;
-- Will the (pseudo)code below work?:
result.user1_id := temp_result[0].id ;
result.user1_name := temp_result[0].name ;
result.user2_id := temp_result[1].id ;
result.user2_name := temp_result[1].name ;
return result ;
END
$$ language plpgsql
Don't use CREATE TYPE to return a polymorphic result. Use and abuse the RECORD type instead. Check it out:
CREATE FUNCTION test_ret(a TEXT, b TEXT) RETURNS RECORD AS $$
DECLARE
ret RECORD;
BEGIN
-- Arbitrary expression to change the first parameter
IF LENGTH(a) < LENGTH(b) THEN
SELECT TRUE, a || b, 'a shorter than b' INTO ret;
ELSE
SELECT FALSE, b || a INTO ret;
END IF;
RETURN ret;
END;$$ LANGUAGE plpgsql;
Pay attention to the fact that it can optionally return two or three columns depending on the input.
test=> SELECT test_ret('foo','barbaz');
test_ret
----------------------------------
(t,foobarbaz,"a shorter than b")
(1 row)
test=> SELECT test_ret('barbaz','foo');
test_ret
----------------------------------
(f,foobarbaz)
(1 row)
This does wreak havoc on code, so do use a consistent number of columns, but it's ridiculously handy for returning optional error messages with the first parameter returning the success of the operation. Rewritten using a consistent number of columns:
CREATE FUNCTION test_ret(a TEXT, b TEXT) RETURNS RECORD AS $$
DECLARE
ret RECORD;
BEGIN
-- Note the CASTING being done for the 2nd and 3rd elements of the RECORD
IF LENGTH(a) < LENGTH(b) THEN
ret := (TRUE, (a || b)::TEXT, 'a shorter than b'::TEXT);
ELSE
ret := (FALSE, (b || a)::TEXT, NULL::TEXT);
END IF;
RETURN ret;
END;$$ LANGUAGE plpgsql;
Almost to epic hotness:
test=> SELECT test_ret('foobar','bar');
test_ret
----------------
(f,barfoobar,)
(1 row)
test=> SELECT test_ret('foo','barbaz');
test_ret
----------------------------------
(t,foobarbaz,"a shorter than b")
(1 row)
But how do you split that out in to multiple rows so that your ORM layer of choice can convert the values in to your language of choice's native data types? The hotness:
test=> SELECT a, b, c FROM test_ret('foo','barbaz') AS (a BOOL, b TEXT, c TEXT);
a | b | c
---+-----------+------------------
t | foobarbaz | a shorter than b
(1 row)
test=> SELECT a, b, c FROM test_ret('foobar','bar') AS (a BOOL, b TEXT, c TEXT);
a | b | c
---+-----------+---
f | barfoobar |
(1 row)
This is one of the coolest and most underused features in PostgreSQL. Please spread the word.
You need to define a new type and define your function to return that type.
CREATE TYPE my_type AS (f1 varchar(10), f2 varchar(10) /* , ... */ );
CREATE OR REPLACE FUNCTION get_object_fields(name text)
RETURNS my_type
AS
$$
DECLARE
result_record my_type;
BEGIN
SELECT f1, f2, f3
INTO result_record.f1, result_record.f2, result_record.f3
FROM table1
WHERE pk_col = 42;
SELECT f3
INTO result_record.f3
FROM table2
WHERE pk_col = 24;
RETURN result_record;
END
$$ LANGUAGE plpgsql;
If you want to return more than one record you need to define the function as returns setof my_type
Update
Another option is to use RETURNS TABLE() instead of creating a TYPE which was introduced in Postgres 8.4
CREATE OR REPLACE FUNCTION get_object_fields(name text)
RETURNS TABLE (f1 varchar(10), f2 varchar(10) /* , ... */ )
...
To return a single row
Simpler with OUT parameters:
CREATE OR REPLACE FUNCTION get_object_fields(_school_id int
, OUT user1_id int
, OUT user1_name varchar(32)
, OUT user2_id int
, OUT user2_name varchar(32)) AS
$func$
BEGIN
SELECT INTO user1_id, user1_name
u.id, u.name
FROM users u
WHERE u.school_id = _school_id
LIMIT 1; -- make sure query returns 1 row - better in a more deterministic way?
user2_id := user1_id + 1; -- some calculation
SELECT INTO user2_name
u.name
FROM users u
WHERE u.id = user2_id;
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM get_object_fields(1);
You don't need to create a type just for the sake of this plpgsql function. It may be useful if you want to bind multiple functions to the same composite type. Else, OUT parameters do the job.
There is no RETURN statement. OUT parameters are returned automatically with this form that returns a single row. RETURN is optional.
Since OUT parameters are visible everywhere inside the function body (and can be used just like any other variable), make sure to table-qualify columns of the same name to avoid naming conflicts! (Better yet, use distinct names to begin with.)
Simpler yet - also to return 0-n rows
Typically, this can be simpler and faster if queries in the function body can be combined. And you can use RETURNS TABLE() (since Postgres 8.4, long before the question was asked) to return 0-n rows.
The example from above can be written as:
CREATE OR REPLACE FUNCTION get_object_fields2(_school_id int)
RETURNS TABLE (user1_id int
, user1_name varchar(32)
, user2_id int
, user2_name varchar(32)) AS
$func$
BEGIN
RETURN QUERY
SELECT u1.id, u1.name, u2.id, u2.name
FROM users u1
JOIN users u2 ON u2.id = u1.id + 1
WHERE u1.school_id = _school_id
LIMIT 1; -- may be optional
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM get_object_fields2(1);
RETURNS TABLE is effectively the same as having a bunch of OUT parameters combined with RETURNS SETOF record, just shorter.
The major difference: this function can return 0, 1 or many rows, while the first version always returns 1 row.
Add LIMIT 1 like demonstrated to only allow 0 or 1 row.
RETURN QUERY is simple way to return results from a query directly.
You can use multiple instances in a single function to add more rows to the output.
db<>fiddle here (demonstrating both)
Varying row-type
If your function is supposed to dynamically return results with a different row-type depending on the input, read more here:
Refactor a PL/pgSQL function to return the output of various SELECT queries
If you have a table with this exact record layout, use its name as a type, otherwise you will have to declare the type explicitly:
CREATE OR REPLACE FUNCTION get_object_fields
(
name text
)
RETURNS mytable
AS
$$
DECLARE f1 INT;
DECLARE f2 INT;
…
DECLARE f8 INT;
DECLARE retval mytable;
BEGIN
-- fetch fields f1, f2 and f3 from table t1
-- fetch fields f4, f5 from table t2
-- fetch fields f6, f7 and f8 from table t3
retval := (f1, f2, …, f8);
RETURN retval;
END
$$ language plpgsql;
You can achieve this by using simply as a returns set of records using return query.
CREATE OR REPLACE FUNCTION schemaName.get_two_users_from_school(schoolid bigint)
RETURNS SETOF record
LANGUAGE plpgsql
AS $function$
begin
return query
SELECT id, name FROM schemaName.user where school_id = schoolid;
end;
$function$
And call this function as : select * from schemaName.get_two_users_from_school(schoolid) as x(a bigint, b varchar);
you can do this using OUT parameter and CROSS JOIN
CREATE OR REPLACE FUNCTION get_object_fields(my_name text, OUT f1 text, OUT f2 text)
AS $$
SELECT t1.name, t2.name
FROM table1 t1
CROSS JOIN table2 t2
WHERE t1.name = my_name AND t2.name = my_name;
$$ LANGUAGE SQL;
then use it as a table:
select get_object_fields( 'Pending') ;
get_object_fields
-------------------
(Pending,code)
(1 row)
or
select * from get_object_fields( 'Pending');
f1 | f
---------+---------
Pending | code
(1 row)
or
select (get_object_fields( 'Pending')).f1;
f1
---------
Pending
(1 row)
CREATE TABLE users(user_id int, school_id int, name text);
insert into users values (1, 10,'alice')
,(5, 10,'boy')
,(13, 10,'cassey')
,(17, 10,'delores')
,(4, 11,'elaine');
I setted the user_id as arbitrary int. The function input parameter is the school_id. So if the school_id is 10 you hope to get the following result:
user_id | name | user_id | name
---------+-------+---------+------
1 | alice | 5 | boy
So your query should be something like:
with a as (
select u1.user_id,
u1.name from users u1
where school_id = 10 order by user_id limit 1),
b as
(select u2.user_id,u2.name from users u2
where school_id = 10 order by user_id limit 1 offset 1 )
select * from a cross JOIN b ;
So let's wrap the query to the plpgsql function.
CREATE OR REPLACE FUNCTION
get_object_fields2(_school_id int)
RETURNS TABLE (user1_id int
, user1_name text
, user2_id int
, user2_name text)
LANGUAGE plpgsql AS
$func$
DECLARE countu integer;
BEGIN
countu := (
select count(*) from users where school_id = _school_id);
IF countu >= 2 THEN
RETURN QUERY
with a as (
select u1.user_id,
u1.name from users u1
where school_id = _school_id
order by user_id limit 1),
b as(
select u2.user_id,u2.name from users u2
where school_id = _school_id
order by user_id limit 1 offset 1 )
select * from a cross JOIN b;
elseif countu = 1 then
return query
select u1.user_id, u1.name,u1.user_id, u1.name
from users u1 where school_id = _school_id;
else
RAISE EXCEPTION 'not found';
end if;
END
$func$;