This is probably really easy, but its driving me crazy.
I have the following function:
CREATE OR REPLACE FUNCTION get_group(serverid BIGINT, name VARCHAR(100)) RETURNS BIGINT
LANGUAGE plpgsql
AS $$
DECLARE
group server_groups%ROWTYPE;
BEGIN
FOR group IN SELECT * FROM server_groups WHERE server_id = serverid AND LOWER(group_name) = LOWER(name) LOOP
RETURN group.id;
END LOOP;
RETURN 0;
END;
$$;
And the following tables:
server
server_id | server_name
----------+---------------
3251623 | ServerOfDoom
5578921 | BestServerEU
server_groups
id | server_id | group_name
---+----------+---------------
1 | 3251623 | aNiceGroup
2 | 5578921 | Valorant
3 | 5578921 | Admins
But when executing my function query I get the following error:
ERROR: syntax error at or near "."
LINE 8: RETURN group.id;
I get that this means I can't just get the id of the group row by saying group.id, but I'm really going crazy finding out how after visiting 100 different sites (maybe I'm just a bad Googler)
How do I get a single attribute of a row in a for loop?
You are overcomplicating things. Your requirement can be achieved with a single SQL query (which can be wrapped in a SQL function):
CREATE OR REPLACE FUNCTION get_group(serverid BIGINT, name VARCHAR(100))
RETURNS BIGINT
as
$$
select coalesce(max(id), 0) as id
from server_groups
where server_id = serverid
and lower(group_name) = lower(name);
$$
language sql
stable;
If the WHERE clause returns no match, max(id) will yield null and the coalesce() will turn that into a 0 (although I would think a null would make much more sense).
If the WHERE clauses matches a row, the query will return the highest ID. As you only expect a single row from the query, the aggregate function won't change that.
If you really want that to be a PL/pgSQL function, you can use the FOUND status as shown in the manual:
CREATE OR REPLACE FUNCTION get_group(serverid BIGINT, name VARCHAR(100))
RETURNS BIGINT
AS $$
declare
group_id int;
BEGIN
select id
into group_id
from server_groups
where server_id = serverid
and lower(group_name) = lower(name);
if found then
return group_id;
else
return 0;
end if;
END;
$$
LANGUAGE plpgsql;
Related
A very similar question here but not quite the same as this one.
I have a function that uses IF statements to determine what type of SELECT query to return.
How can I declare what a CREATE FUNCTION statment should return when I will never know the exact columns a SELECT query within it might return? That is, I can't setup a RETURNS TABLE declaration with a list of columns because I don't know which columns might come back. All I know is that I definitely will want a table of results to be returned.
Here is my function (uncompleted, pseudo):
CREATE OR REPLACE FUNCTION functiona(_url character varying DEFAULT NULL)
RETURNS -- what type? if TABLE how do I know what columns to specify
LANGUAGE plpgsql
AS
$$
DECLARE
_urltypeid int;
BEGIN
IF _url IS NOT NULL
THEN
_urltypeid := reference.urltype(_url);
IF _urltypeid = 1
THEN
RETURN QUERY
SELECT location, auxiliary, response FROM tablea -- unique columns from one table
END IF;
IF _urltypeid = 2
THEN
RETURN QUERY
SELECT ip, location, host, authority FROM tableb -- unique columns from another table
END IF;
END IF;
END;
$$;
I come from a MS SQL Server background where I don't have to specify in the CREATE FUNCTIONstatement what I'm returning, hence this is very confusing for me.
Not an answer, but an explanation of why answer from #JonathanJacobson will not work using a simple example:
\d animals
Table "public.animals"
Column | Type | Collation | Nullable | Default
--------+------------------------+-----------+----------+---------
id | integer | | not null |
cond | character varying(200) | | not null |
animal | character varying(200) | | not null |
CREATE OR REPLACE FUNCTION public.animal(a_type character varying)
RETURNS record
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT row(id, cond, animal) FROM animals where animal = a_type;
END;
$function$
select * from animal('cat');
ERROR: a column definition list is required for functions returning "record"
LINE 1: select * from animal('cat');
CREATE OR REPLACE FUNCTION public.animal(a_type character varying)
RETURNS SETOF record
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT id, cond, animal FROM animals where animal = a_type;
END;
$function$
;
select * from animal('cat') as t(i integer, c varchar, a varchar);
i | c | a
---+------+-----
1 | fat | cat
2 | slim | cat
6 | big | cat
In order to use the output of a function returning a record or setof record you need to declare the output fields and types when you run the function.
You could use the record type. Not tested.
CREATE OR REPLACE FUNCTION functiona(_url character varying DEFAULT NULL)
RETURNS record
LANGUAGE plpgsql
AS
$$
DECLARE
_broadcasttypeid int;
BEGIN
IF _url IS NOT NULL
THEN
_urltypeid := reference.urltype(_url);
IF _urltypeid = 1
THEN
RETURN
(SELECT row(location, auxiliary, response) FROM tablea);
END IF;
IF _urltypeid = 2
THEN
RETURN
(SELECT row(ip, location, host, authority) FROM tableb);
END IF;
END IF;
END;
$$;
Other composite types, such as jsonb and hstore are also a solution.
My table will look like this-
id | expression | unit_cost | demand |total_cost|
------ | -------------------------------------| ----------|--------|----------|
1 | (unit_cost*4)*demand | 5 |100 | |
2 | (unit_cost*(8/100)demand)*demand | 10 |50 | |
Now, I want to calculate total_cost based on the expression column using the other columns as specified in the expression. Changes in schema can be done, its just a sample to show what i actually want to do.
Note: expressions will be different for each row
You can use a function like this:
create or replace function eval(p_row the_table)
returns integer
as
$body$
declare
l_result integer;
l_sql text;
begin
l_sql := format('select %s from (values ($1, $2) ) as t(unit_cost, demand)',
p_row.expression);
execute l_sql
into l_result
using p_row.unit_cost, p_row.demand;
return l_result;
end;
$body$
language plpgsql;
(You need to replace the_table with the actual name of your table)
I decided to pass the complete row of the table as the parameter, so that you don't need to change the anything if you decide to use more columns from the table in your expressions.
The generated SQL looks like this (e.g. for the first row):
select (unit_cost*4)*demand
from ( values ($1, $2)) as t(unit_cost, demand);
The parameters in the values clause are then passed with the using ... part to make sure they are treated with the correct data types, which means it's executed as:
select (unit_cost*4)*demand
from ( values (5, 100)) as t(unit_cost, demand);
You can use it like this:
select t.id, t.unit_cost, t.demand, eval(t) as total_cost
from the_table t;
Note the table alias that is used to pass the row to the function.
If you know that the input values never change, you can also pass them directly:
create or replace function eval(p_expression text, p_demand int, p_unit_cost int)
returns integer
as
$body$
declare
l_result integer;
l_sql text;
begin
l_sql := format('select %s from (values ($1, $2) ) as t(unit_cost, demand)',
p_expression);
execute l_sql
into l_result
using p_unit_cost, p_demand;
return l_result;
end;
$body$
language plpgsql;
Then call it like this:
select id, unit_cost, demand, eval(t.expression, t.demand, t.unit_cost) as total_cost
from the_table t;
The first version (where you pass the complete row) has the advantage that you can never mix up the order of the parameters and accidentally pass the demand as the unit cost.
Online example
reading about how to make an SP that returns the results of a query it seems I must do this kind of thing (from tutorial site)
CREATE OR REPLACE FUNCTION show_cities() RETURNS refcursor AS $$
DECLARE
ref refcursor; -- Declare a cursor variable
BEGIN
OPEN ref FOR SELECT city, state FROM cities; -- Open a cursor
RETURN ref; -- Return the cursor to the caller
END;
$$ LANGUAGE plpgsql;
OK, I get this, but I want to pass the SQL in as a paramter so I need to do (I think)
EXECUTE mysql ......
But I dont see how to make EXECUTE return a cursor
EDIT:
OK now I see that I misunderstood what the non dynamic case does. I expected to be able to do select show_cities() and have it do that same thing as SELECT city, state FROM cities, it does not. Of course now that I think about it this isnt surprising. I want to return the actual set of records.
In your case smth like:
t=# CREATE OR REPLACE FUNCTION data_of(_tbl_type anyelement)
RETURNS SETOF anyelement AS
$func$
BEGIN
RETURN QUERY EXECUTE format('
SELECT *
FROM %s -- pg_typeof returns regtype, quoted automatically
WHERE true /*or some filter in additional arguments or so */
ORDER BY true /*or some filter in additional arguments or so */'
, pg_typeof(_tbl_type))
;
END
$func$ LANGUAGE plpgsql;
CREATE FUNCTION
would work:
t=# create table city(i serial, cn text);
CREATE TABLE
t=# insert into city(cn) values ('Moscow'), ('Barcelona'),('Visaginas');
INSERT 0 3
t=# SELECT * FROM data_of(NULL::city);
i | cn
---+-----------
1 | Moscow
2 | Barcelona
3 | Visaginas
(3 rows)
all credits to Erwin with his https://stackoverflow.com/a/11751557/5315974 which is one obligatory reading
When I try to compile this function:
CREATE OR REPLACE FUNCTION test_proc4(sr_num bigint)
RETURNS TABLE(sr_number bigint, product_serial_number varchar(35))
AS $$
BEGIN
RETURN QUERY SELECT select sr_number,product_serial_number from temp_table where sr_number=sr_num
END;
$$
LANGUAGE 'plpgsql' VOLATILE;
Why do I get this error?
RETURN cannot have a parameter in function returning set; use RETURN NEXT at or near "QUERY"
I am using postgres version 8.4.
Aside from a typo (you duplicated select, and you didn't terminate the RETURN statement with a semicolon), I think you were quite close - just needed to disambiguate the table columns within the query by qualifying them with the table name. Try this (reformatted hopefully to improve readability):
CREATE OR REPLACE FUNCTION test_proc4(sr_num bigint)
RETURNS TABLE(sr_number bigint, product_serial_number varchar(35)) AS $$
BEGIN
RETURN QUERY
SELECT
temp_table.sr_number, temp_table.product_serial_number
from temp_table
where temp_table.sr_number=sr_num;
END;
$$ LANGUAGE 'plpgsql' VOLATILE;
Caveat: I only tested this in PG 9.4, so haven't tested it in your version yet (which is no longer supported, I might add). In case there are issues regarding PLPGSQL implementation between your version and 9.4, you can try this form as an alternative:
CREATE OR REPLACE FUNCTION test_proc4(sr_num bigint)
RETURNS TABLE(sr_number bigint, product_serial_number varchar(35)) AS $$
BEGIN
FOR sr_number, product_serial_number IN
SELECT
temp_table.sr_number, temp_table.product_serial_number
from temp_table
where temp_table.sr_number=sr_num
LOOP
RETURN NEXT;
END LOOP;
RETURN;
END;
$$ LANGUAGE 'plpgsql' VOLATILE;
A small sanity check using a table I populated with dummy data:
postgres=# select * from temp_table;
sr_number | product_serial_number
-----------+-----------------------
1 | product 1
2 | product 2
2 | another product 2
(3 rows)
postgres=# select * from test_proc4(1);
sr_number | product_serial_number
-----------+-----------------------
1 | product 1
(1 row)
postgres=# select * from test_proc4(2);
sr_number | product_serial_number
-----------+-----------------------
2 | product 2
2 | another product 2
(2 rows)
I used EXECUTE(for dynamic sql) and SETOF(result is returning as list), but it is the wrong :(
create table test as
select 1 id, 'safd' data1,'sagd' data2
union
select 2 id, 'hdfg' data1,'sdsf' data2;
create or replace function test2(a varchar) returns SETOF record as
$BODY$
declare x record;
begin
for x in execute a loop
RETURN NEXT x;
end loop;
return;
end;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
select * from test2('select * from test');
You will have to know in advance the structure of the returned record
select * from test2('select * from test') s(a int, b text, c text);
a | b | c
---+------+------
1 | safd | sagd
2 | hdfg | sdsf
Or if the returned set will always be a set of the test table then use the Akash's proposed solution.
replace
create or replace function test2(a varchar) returns SETOF RECORD as
with
create or replace function test2(a varchar) returns SETOF test as
^^^^ name of table (it specifies the datatypes of the set)
You need to add some OUT params.
CREATE FUNCTION test2(a character varying, OUT id integer, OUT data1 text, OUT data2 text) RETURNS SETOF record
LANGUAGE plpgsql
AS $$
begin
RETURN QUERY EXECUTE a;
end;
$$;