Related
I have a problem. There is a function, that counts total budget of department, including departments lower down the hierarchy:
CREATE OR REPLACE FUNCTION PUBLIC.DEPT_BUDGET (DNO BPCHAR(3))
RETURNS TABLE (
TOT DECIMAL(12,2)
)
AS $DEPT_BUDGET$
DECLARE sumb DECIMAL(12, 2);
DECLARE rdno BPCHAR(3)[];
DECLARE cnt INTEGER;
DECLARE I BPCHAR(3);
BEGIN
tot = 0;
SELECT "BUDGET" FROM department WHERE dept_no = dno INTO tot;
SELECT count("BUDGET") FROM department WHERE head_dept = dno INTO cnt;
IF cnt = 0
THEN RETURN QUERY SELECT "BUDGET" FROM department WHERE dept_no = dno;
END IF;
SELECT
ARRAY_AGG(dept_no)
FROM
department
WHERE
head_dept = dno
INTO
rdno;
FOREACH I IN ARRAY rdno
LOOP
SELECT * FROM DEPT_BUDGET(I) INTO SUMB;
tot = tot + sumb;
END LOOP;
END; $DEPT_BUDGET$ LANGUAGE plpgsql;
The dept_no has bpchar(3) type. When I'm trying to call a function SELECT public.dept_budget('000'::VARCHAR); , I got an error:
SQL Error [42883]: ERROR: function dept_budget(integer) does not exist. No function matches the given name and argument types. You might need to add explicit type casts.
When I change in-type parameter on bpchar or char, I got another error:
SQL Error [22004]: ERROR: FOREACH expression must not be null.
I don't understand, why forced typization doesn't work. What should I do?
Types of data
UPD: Yeah, there is bpchar, but I have already tried to change everywhere VARCHAR(3) on BPCHAR(3), and there is still an error.
This function could be written in sql instead of plpgsql. Try this :
CREATE OR REPLACE FUNCTION PUBLIC.DEPT_BUDGET (DNO VARCHAR)
RETURNS DECIMAL(12,2) LANGUAGE sql AS $$
SELECT sum( CASE
WHEN dept_no = dno
THEN budget
ELSE dept_budget(dept_no)
END
) :: decimal(12,2)
FROM department
WHERE dept_no = dno
OR head_dept = dno ;
$$ ;
I need to create a table dynamically based on different values comes from distinct values of type column from tbl1. Please let me know if it is possible using cursor and function below.
Creating tbl1 with a columns id, type and value.
Creating tbl2 with a columns id ,gender.
Function retrieving values into final table using cursors. (creating temp table dual for checking what values are being passing.)
create table tbl1 (
id int not null,
type varchar not null,
value varchar
);
create table tbl2 (
id int not null,
gender varchar not null
);
commit;
insert into tbl1 values (1,'name','A'),(2,'name','B'),(1,'age','10'),(3,'name','C');
insert into tbl2 values (1,'M'),(2,'F');
commit;
--the below crosstab didn't work
SELECT id
, COALESCE(name, max(name) OVER w)
, COALESCE(age, max(age) OVER w)
FROM crosstab(
'SELECT id::text || row_number() OVER (PARTITION BY id, type ORDER BY value) * -1 AS ext_id
, id, type, value
FROM tbl1
ORDER BY ext_id, type, value'
,$$VALUES ('name'::text), ('age') $$
) AS ct (xid text, id int, name text, age int)
WINDOW w AS (PARTITION BY id);
-- FUNCTION: SELECT public.Finaltblfunc1()
-- DROP FUNCTION public.Finaltblfunc1();
CREATE OR REPLACE FUNCTION public.Finaltblfunc1()
RETURNS setof refcursor
LANGUAGE 'plpgsql'
COST 100
VOLATILE
AS $BODY$
DECLARE
/* Declare variables. */
P_id NUMERIC(10,0);
P_name VARCHAR(20);
P_age VARCHAR(3);
P_gender VARCHAR(1);
v_leng INTEGER;
v_leng1 INTEGER;
v_j bigint;
v_k VARCHAR(10);
/* Declare cursors. */
sourcerefcur1 CURSOR FOR SELECT t1.id,
(CASE WHEN t1.type = 'name' THEN t1.value ELSE '' END) AS name,
(CASE WHEN t1.type = 'age' THEN t1.value ELSE '' END) AS age,
t2.gender
FROM tbl1 t1 full outer join tbl2 t2 on t1.id = t2.id;
temprefcur1 CURSOR FOR SELECT distinct t1.type FROM tbl1 t1;
--targetrefcur2 REFCURSOR;
/* Declare SQL string variables. */
SQL_STR1 VARCHAR(200):= 'SELECT count(distinct table_name)
FROM information_schema.tables
WHERE table_schema = ''public'' and table_name = ''finaltable''';
/* Declare error handling variables. */
err_num TEXT;
err_msg TEXT;
BEGIN
/* tables exists or not */
EXECUTE SQL_STR1 INTO v_j;
RAISE INFO 'Finaltable check:%',v_j;
IF (v_j = 0) THEN
--Creating a Final Table
create table finaltable (
id NUMERIC(10,0),
name varchar(50),
age varchar(3),
gender varchar(1)
);
ELSE
--do nothing
END IF;
v_leng := 0;
--open the cursor temprefcur1
OPEN temprefcur1;
loop
--fetch next from temprefcur1 into respective parameters;
fetch next from temprefcur1 into v_k;
-- exit when no more row to fetch
EXIT WHEN NOT FOUND;
v_leng = v_leng +1;
raise notice 'v_k:%',v_k;
raise notice 'v_leng:%',v_leng;
end loop;
return next temprefcur1;
-- Close the cursor
CLOSE temprefcur1;
v_leng1 := 0;
--open the cursor sourcerefcur1
OPEN sourcerefcur1;
loop
--fetch next from sourcerefcur1 into respective parameters;
fetch next from sourcerefcur1 into P_id,P_name,P_age,P_gender;
-- exit when no more row to fetch
EXIT WHEN NOT FOUND;
v_leng1 = v_leng1 +1;
RAISE INFO 'P_id: %',P_id; --, E'\n';
RAISE INFO 'P_name: %',P_name; --, E'\n';
RAISE INFO 'P_age: %',P_age; --, E'\n';
RAISE INFO 'P_gender: %',P_gender; --, E'\n';
RAISE INFO 'length: %',v_leng1; --, E'\n';
raise notice 'step insert';
insert into finaltable values (P_id,P_name,P_age,P_gender);
insert into dual values (P_id),(P_name),(P_age),(P_gender);
insert into dual values (v_leng1);
raise notice 'after step insert';
end loop;
return next sourcerefcur1;
--close sourcerefcur1
close sourcerefcur1;
EXCEPTION
WHEN OTHERS THEN
err_num := SQLSTATE;
err_msg := SUBSTR(SQLERRM,1,100);
RAISE INFO 'Error: % %', err_num, err_msg;
END;
$BODY$;
ALTER FUNCTION public.Finaltblfunc1()
OWNER TO postgres;
You may have been (hugely) over-complicating things. This should basically do it all:
CREATE TABLE IF NOT EXISTS finaltable (
id bigint
, name text
, age int
, gender text
);
INSERT INTO finaltable(id, name, age, gender)
SELECT *
FROM crosstab(
$$SELECT id, type , value FROM tbl1
UNION ALL
SELECT id, 'gender', gender FROM tbl2
ORDER BY id$$
,$$VALUES ('name'), ('age'), ('gender')$$
) AS ct (id int, name text, age int, gender text);
Result:
id | name | age | gender
-: | :--- | ---: | :-----
1 | A | 10 | M
2 | B | null | F
3 | C | null | null
db<>fiddle here
Not sure what the added COALESCE was supposed to achieve. I stripped it.
Basics:
PostgreSQL Crosstab Query
Aside: age as table column is subject to bitrot. Store birthdays instead (or similar).
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?
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?
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$;