This question already has an answer here:
pgSQL show an error in its function before the SELECT query
(1 answer)
Closed 1 year ago.
When I work with Microsoft SQL Server databases, I sometimes return multiple result sets from stored procedures. I often return so many that it becomes hard to follow which is which. To solve this problem, I follow a convention I learned from a colleague: The 1st result set is a "map", which defines names for the 2nd and other result sets. It has a single record, where every field name is the name of a result set and the corresponding field value is its index in the returned array of result sets. Client code accesses specific result sets by finding out the index by name first.
The following simple example shows the idea:
create or alter procedure divide
#a int,
#b int
as
begin
declare
#error int = 0
-- Name-to-index map
select
1 as result,
2 as error
-- Result
if #b = 0
begin
set #error = 1
select
null as result
end
else
begin
select
#a / #b as result
end
-- Error
select
#error as error
end
In this example, the first result set (index: 0) gives that there are 2 more result sets: one called "result" (index: 1) and another called "error" (index: 2). Both contain only one record: the result of the division and the error code, respectively.
Example call #1:
exec divide #a = 91, #b = 13
Result sets in JSON format:
[
[{ result: 1, error: 2 }],
[{ result: 7 }],
[{ error: 0 }]
]
Example call #2:
exec divide #a = 91, #b = 0
Result sets in JSON format:
[
[{ result: 1, error: 2 }],
[{ result: null }],
[{ error: 1 }]
]
I tried to port this technique to PostgreSQL 14 using the official documentation and especially this page. I came up with this:
create or replace function divide(
a integer,
b integer
)
returns setof refcursor
language sql as
$$
declare
ref1 refcursor;
ref2 refcursor;
ref3 refcursor;
error int := 0;
begin
-- Name-to-index map
open ref1 for select
1 as result,
2 as error;
return next ref1;
-- Result
if b = 0 then
error := 1;
open ref2 for select
null as result;
else
open ref2 for select
a / b as result;
end if;
return next ref2;
-- Error
open ref3 for select
error;
return next ref3;
end;
$$;
Unfortunately, I get an error: syntax error at or near "refcursor", referring to the refcursor in the 1st line after declare.
You used the wrong language declaration. Your procedure is in plpgsql but you declared it as plain sql through language sql statement at the top.
Replacing
create or replace function divide(
a integer,
b integer
)
returns setof refcursor
language sql as
with
create or replace function divide(
a integer,
b integer
)
returns setof refcursor
language plpgsql as
Solves the problem.
Related
Suppose I have:
CREATE TYPE compfoo AS (f1 int, f2 text);
And I create a table foo containing two columns: fooid and fooname, corresponding to the fields of compfoo, later I insert some records 1, aa, 2, bb, 3, cc
Then, I define a PL/pgSQL function (more or less as follows:)
create or replace function foo_query()
returns text
language plpgsql
as $$
declare
r compfoo;
arr compfoo [];
footemp compfoo;
result text;
begin
for r in
select * from foo where fooid = 1 OR fooid = 2
loop
arr := array_append(arr, r);
end loop;
foreach footemp in array arr
loop
select footemp.f1 into result where footemp.f1 = 1;
end loop;
return result;
end;
$$
Where I query first foo by column names and save the results into arr, an array of compfoo. Later, I iterate over arr and try to query the elements by their fieldnames as defined in compfoo.
I don't get an error per se in Postgres but the result of my function is null.
What am I doing wrong?
The RAISE NOTICE should be your best friend. You can print the result of some variables at some points of your code. The basic issue are not well initialized values. The arr variable is initialized by NULL value, and any operation over NULL is NULL again.
Another problem is in select footemp.f1 into result where footemp.f1 = 1; statement. SELECT INTO in Postgres overwrite the target variable by NULL value when an result is empty. In second iteration, the result of this query is empty set, and the result variable is set on NULL.
The most big problem of your example is style of programming. You use ISAM style, and your code can be terrible slow.
Don't use array_append in cycle, when you can use array_agg function in query, and you don't need cycle,
Don't use SELECT INTO when you don't read data from tables,
Don't try to repeat Oracle' pattern BULK COLLECT and FOREACH read over collection. PostgreSQL is not Oracle, uses very different architecture, and this pattern doesn't increase performance (like on Oracle), but probably you will lost some performance.
Your fixed code can looks like:
CREATE OR REPLACE FUNCTION public.foo_query()
RETURNS text
LANGUAGE plpgsql
AS $function$
declare
r compfoo;
arr compfoo [] default '{}'; --<<<
footemp compfoo;
result text;
begin
for r in
select * from foo where fooid = 1 or fooid = 2
loop
arr := array_append(arr, r);
end loop;
foreach footemp in array arr
loop
if footemp.f1 = 1 then --<<<
result := footemp.f1;
end if;
end loop;
return result;
end;
$function$
postgres-# ;
It returns expected result. But it is perfect example how don't write stored procedures. Don't try to replace SQL in stored procedures. All code of this procedure can be replaced just by one query. In the end this code can be very slow on bigger data.
Relatively new to Postgres, and been having trouble subtracting a value from a NUMERIC(4,2) value type in an update statement. The following code:
UPDATE Tickets
SET ticketPrice = ticketPrice-3
FROM Showings
WHERE Showings.priceCode = modTicket
Elicits the following error:
ERROR: numeric field overflow
Detail: A field with precision 4, scale 2 must round to an absolute value less than 10^2.
ticketPrice has the value type of NUMERIC(4,2). How do I make this subtraction? There are no possible values that this subtraction would cause to extend past two decimal points, or in the negatives at all. The only values that this subtraction applies to are 5.00, 3.50, and 8.00.
You could try to find out the error source like this:
do $$
declare r record;
foo numeric(4,2);
begin
for r in (select t.* from Tickets as t, Showings as s where s.priceCode = t.modTicket) loop
foo := r.ticketPrice - 3;
end loop;
exception
when others then raise notice '%', r;
raise;
end $$;
Example:
do $$
declare r record;
foo numeric(1);
begin
for r in (with t(x,y) as (values(1,2),(3,4)) select * from t) loop
foo := r.y + 7;
end loop;
exception
when others then raise notice '%', r;
raise;
end $$;
Output:
NOTICE: (3,4)
ERROR: numeric field overflow
DETAIL: A field with precision 1, scale 0 must round to an absolute value less than 10^1.
CONTEXT: PL/pgSQL function inline_code_block line 1 at assignment
UPDATE Tickets as t
SET ticketPrice = ticketPrice-3
FROM Showings as s
WHERE s.priceCode = t.modTicket
Documentation of UPDATE Query
In a function in plpgsql language and having:
"col" CHARACTER VARYING = 'a'; (can be 'b')
"rec" RECORD; Holding records from: (SELECT 1 AS "a", 2 AS "b")
"res" INTEGER;
I need to reference the named column in "col", from "rec". So if "col" has 'b' I will reference "rec"."b", and then save its value into "res".
You cannot reference the columns of an anonymous record type by name in plpgsql. Even though you spell out column aliases in your example in the SELECT statement, those are just noise and discarded.
If you want to reference elements of a record type by name, you need to use a well-known type. Either create and use a type:
CREATE TYPE my_composite_type(a int, b int);
Or use the row type associated with any existing table. You can just write the table name as data type.
DECLARE
rec my_composite_type;
...
Then you need a conditional statement or dynamic SQL to use the value of "col" as identifier.
Conditional statement:
IF col = 'a' THEN
res := rec.a;
ELSIF col = 'b' THEN
res := rec.b;
ELSE
RAISE EXCEPTION 'Unexpected value in variable "col": %', col;
END IF;
For just two possible cases, that's the way to go.
Or dynamic SQL:
EXECUTE 'SELECT $1.' || col
INTO res
USING rec;
I don't see a problem here, but be wary of SQL injection with dynamic SQL. If col can hold arbitrary data, you need to escape it with quote_ident() or format()
Demo
Demonstrating the more powerful, but also trickier variant with dynamic SQL.
Quick & dirty way to create a (temporary!) known type for testing:
CREATE TEMP TABLE rec_ab(a int, b int);
Function:
CREATE OR REPLACE FUNCTION f_test()
RETURNS integer AS
$func$
DECLARE
col text := 'a'; -- can be 'b'
rec rec_ab;
res int;
BEGIN
rec := '(1, 2)'::rec_ab;
EXECUTE 'SELECT $1.' || col
INTO res
USING rec;
RETURN res;
END
$func$
LANGUAGE plpgsql VOLATILE;
Call:
SELECT f_test();
Returns:
f_test
----
1
I can't find anything in the PostgreSQL documentation that shows how to declare a record, or row, while declaring the tuple structure at the same time. If you don't define you tuple structure you get the error "The tuple structure of a not-yet-assigned record is indeterminate".
This is what I'm doing now, which works fine, but there must be a better way to do it.
CREATE OR REPLACE FUNCTION my_func()
RETURNS TABLE (
"a" integer,
"b" varchar
) AS $$
DECLARE r record;
BEGIN
CREATE TEMP TABLE tmp_t (
"a" integer,
"b" varchar
);
-- Define the tuple structure of r by SELECTing an empty row into it.
-- Is there a more straight-forward way of doing this?
SELECT * INTO r
FROM tmp_t;
-- Now I can assign values to the record.
r.a := at.something FROM "another_table" at
WHERE at.some_id = 1;
-- A related question is - how do I return the single record 'r' from
-- this function?
-- This works:
RETURN QUERY
SELECT * FROM tmp_t;
-- But this doesn't:
RETURN r;
-- ERROR: RETURN cannot have a parameter in function returning set
END; $$ LANGUAGE plpgsql;
You are mixing the syntax for returning SETOF values with syntax for returning a single row or value.
-- A related question is - how do I return the single record 'r' from
When you declare a function with RETURNS TABLE, you have to use RETURN NEXT in the body to return a row (or scalar value). And if you want to use a record variable with that it has to match the return type. Refer to the code examples further down.
Return a single value or row
If you just want to return a single row, there is no need for a record of undefined type. #Kevin already demonstrated two ways. I'll add a simplified version with OUT parameters:
CREATE OR REPLACE FUNCTION my_func(OUT a integer, OUT b text)
AS
$func$
BEGIN
a := ...;
b := ...;
END
$func$ LANGUAGE plpgsql;
You don't even need to add RETURN; in the function body, the value of the declared OUT parameters will be returned automatically at the end of the function - NULL for any parameter that has not been assigned.
And you don't need to declare RETURNS RECORD because that's already clear from the OUT parameters.
Return a set of rows
If you actually want to return multiple rows (including the possibility for 0 or 1 row), you can define the return type as RETURNS ...
SETOF some_type, where some_type can be any registered scalar or composite type.
TABLE (col1 type1, col2 type2) - an ad-hoc row type definition.
SETOF record plus OUT parameters to define column names andtypes.
100% equivalent to RETURNS TABLE.
SETOF record without further definition. But then the returned rows are undefined and you need to include a column definition list with every call (see example).
The manual about the record type:
Record variables are similar to row-type variables, but they have no
predefined structure. They take on the actual row structure of the
row they are assigned during a SELECT or FOR command.
There is more, read the manual.
You can use a record variable without assigning a defined type, you can even return such undefined records:
CREATE OR REPLACE FUNCTION my_func()
RETURNS SETOF record AS
$func$
DECLARE
r record;
BEGIN
r := (1::int, 'foo'::text); RETURN NEXT r; -- works with undefined record
r := (2::int, 'bar'::text); RETURN NEXT r;
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM my_func() AS x(a int, b text);
But this is very unwieldy as you have to provide the column definition list with every call. It can generally be replaced with something more elegant:
If you know the type at time of function creation, declare it right away (RETURNS TABLE or friends).
CREATE OR REPLACE FUNCTION my_func()
RETURNS SETOF tbl_or_type AS
$func$
DECLARE
r tbl_or_type;
BEGIN
SELECT INTO tbl_or_type * FROM tbl WHERE id = 10;
RETURN NEXT r; -- type matches
SELECT INTO tbl_or_type * FROM tbl WHERE id = 12;
RETURN NEXT r;
-- Or simpler:
RETURN QUERY
SELECT * FROM tbl WHERE id = 14;
END
$func$ LANGUAGE plpgsql;
If you know the type at time of the function call, there are more elegant ways using polymorphic types:
Refactor a PL/pgSQL function to return the output of various SELECT queries
Your question is unclear as to what you need exactly.
There might be some way that avoids the explicit type declaration, but offhand the best I can come up with is:
CREATE TYPE my_func_return AS (
a integer,
b varchar
);
CREATE OR REPLACE FUNCTION my_func()
RETURNS my_func_return AS $$
DECLARE
r my_func_return;
BEGIN
SELECT 1, 'one' INTO r.a, r.b;
RETURN r;
END; $$ LANGUAGE plpgsql;
Oh, I almost forgot the simplest way to do this:
CREATE OR REPLACE FUNCTION my_func2(out a int, out b text)
RETURNS RECORD AS $$
BEGIN
SELECT 1, 'one' INTO a, b;
RETURN;
END; $$ LANGUAGE plpgsql;
It is much easier to use OUT parameters rather than a record. If iteratively building a set of records (a table) use RETURN NEXT. If generating from a query, use RETURN QUERY. See:
https://stackoverflow.com/a/955289/398670
and:
http://www.postgresql.org/docs/current/static/plpgsql-declarations.html
http://www.postgresql.org/docs/current/static/sql-createfunction.html
http://www.postgresonline.com/journal/archives/129-Use-of-OUT-and-INOUT-Parameters.html
Think:
CREATE OR REPLACE FUNCTION my_func(OUT a integer, OUT b varchar) RETURNS SETOF RECORD AS $$
BEGIN
-- Assign a and b, RETURN NEXT, repeat. when done, RETURN.
END;
$$ LANGUAGE 'plpgsql';
Here is the code
CREATE OR REPLACE FUNCTION primes (IN integer) RETURNS TEXT AS $$
DECLARE
counter INTEGER = $1;
primes int [];
mycount int;
BEGIN
WHILE counter != 0 LOOP
mycount := count(primes);
array_append(primes [counter], mycount);
counter := counter - 1;
END LOOP;
RETURN array_to_text(primes[], ',');
END;
$$
LANGUAGE 'plpgsql'
This is me developing the beginnings of a prime generating function. I am trying to simply get it to return the 'count' of the array. So if I pass '7' into the function I should get back [0, 1, 2, 3, 4, 5, 6].
But when I try to create this function I get
SQL Error: ERROR: syntax error at or near "array_append" LINE 1: array_append( $1 [ $2 ], $3 )
^ QUERY: array_append( $1 [ $2 ], $3 ) CONTEXT: SQL statement in PL/PgSQL function "primes" near line 8
I am a newbie with postgres functions. I am not understanding why I cannnot get this array to work properly.
Again all I want is to just fill this array up correctly with the 'current' count of the array. (It's more to just help me understand that it is in fact doing the loop correctly and is counting it correctly).
Thank you for your help.
From the fine manual:
Function: array_append(anyarray, anyelement)
Return Type: anyarray
Description: append an element to the end of an array
So array_append returns an array and you need to assign that return value to something. Also, I think you want array_to_string at the end of your function, not array_to_text. And primes is an array so you want array_append(primes, mycount) rather than trying to append to an entry in primes.
CREATE OR REPLACE FUNCTION primes (IN integer) RETURNS TEXT AS $$
DECLARE
counter INTEGER = $1;
primes int [];
mycount int;
BEGIN
WHILE counter != 0 LOOP
mycount := count(primes);
primes := array_append(primes, mycount);
counter := counter - 1;
END LOOP;
RETURN array_to_string(primes, ',');
END;
$$ LANGUAGE 'plpgsql';
I don't know what you intend mycount := count(primes); to do, perhaps you meant to say mycount := array_length(primes, 1); so that you would get a sequence of consecutive integers in primes.