Return setof record with 1 row - sql

I'm altering a PLPGSQL function and I'm having a small problem. First let me post it's declaration:
CREATE OR REPLACE FUNCTION permissions(_principal text)
RETURNS SETOF record AS
$BODY$
DECLARE
id integer := 0;
rolerow record ;
In this function there are a few cases, controled by IF statements, and in all of them, the return is the UNION of more than 1 query, such as this:
FOR rolerow IN (
(SELECT 'role1' AS role FROM table1 WHERE id = table1.id)
UNION (SELECT 'role2' AS role FROM table2 WHERE id = table2.id)
UNION (SELECT 'role3' AS role FROM table3 WHERE id = table3.id)
)
LOOP
RETURN NEXT rolerow;
END LOOP;
RETURN;
And it all works fine, but in one case, I need to return a single query result, that would be a SETOF record but with only 1 item, so I did it like this:
FOR rolerow IN (
SELECT 'role4' AS role FROM table4 WHERE id = table4.id
)
LOOP
RETURN NEXT rolerow;
END LOOP;
RETURN;
I also tried
RETURN QUERY SELECT 'role4' AS role FROM table4 WHERE id = table4.id;
But in both cases I get the same error as a response:
ERROR: structure of query does not match function result type
DETAIL: Returned type unknown does not match expected type text in column 1.
Does anyone have any idea how I can fix this?
I'll provide extra information in case this isn't enough.

You need an explicit cast for the string literal 'role4', which is not typed (type "unknown") unlike you seem to expect:
SELECT 'role4'::text AS role FROM ...
Generally, looping is more expensive for your simple examples. Use RETURN QUERY like you already tested.

Related

How do I return a set of jsonb objects

I have defined a function like so:
create or replace function get_those_other_strings(pid int)
returns setof text
language sql
returns null on null input
as $$
select array_agg(t1.text_column) as val
from table1 t1
left join table2 t2 on t1.id = t2.value_id
where t2.other_value_id = pid
$$;
It takes an input (an id in a specific table), and returns a set of text values from other tables that are related. It works just fine.
Now, I try to create a similar function, with the only difference is that the type of return value is not text but jsonb:
create or replace function get_those_other_jsonbs(pid int)
returns setof jsonb
language sql
returns null on null input
as $$
select array_agg(t1.jsonb_column) as val
from table1 t1
left join table2 t2 on t1.id = t2.value_id
where t2.other_value_id = pid
$$;
And suddenly I can't do that. When trying to save this 2nd function, I get return type mismatch in function declared to return jsonb.
When the query from the function is run on its own, it works perfectly fine, and returns something like:
[{"key1":"val1","key2":"val2","ke2":"val3"},{"key1":"val3","key2":"val5","key3":"val8"}]
What am I doing wrong?

Reference a parameter in Postgres function

The table org has a column called npi. Why does query1 work and query2 not?
Query 1 -
CREATE OR REPLACE FUNCTION check (npi TEXT)
RETURNS BOOLEAN AS $$
DECLARE
pass_npi TEXT;
BEGIN
pass_npi := npi;
SELECT 1
FROM org doc
WHERE doc.npi = pass_npi
;
RETURN 1;
END $$
Query 2 -
CREATE OR REPLACE FUNCTION check (npi TEXT)
RETURNS BOOLEAN AS $$
BEGIN
SELECT 1
FROM org doc
WHERE doc.npi = npi
;
RETURN 1;
END $$
ERROR -
Ambigious column name NPI
Because in the second case it is unclear if npi is the table column (that would be a valid, if useless statement) or the function parameter.
There are three solutions apart from the one in your first query:
The best one: use function parameters that have names different from table columns. This can be done by using a prefix:
CREATE FUNCTION check (p_npi TEXT) RETURNS boolean AS
...
SELECT ...
WHERE doc.npi = p_npi
Use the ALIAS command to “rename” the parameter:
CREATE FUNCTION check (npi TEXT) RETURNS boolean AS
$$DECLARE
p_npi ALIAS FOR npi;
BEGIN
...
SELECT ...
WHERE doc.npi = p_npi
Qualify the parameter with the function name:
CREATE FUNCTION check (npi TEXT) RETURNS boolean AS
...
SELECT ...
WHERE doc.npi = check.npi
What happens is that in Query2 you are comparing the field the doc.npi field with it, it is the same to say doc.npi and to say npi, for that reason it shows you that the sentence is ambiguous, on the contrary case in Query1 you are comparing the doc.npi field with a different field that is the pass_npi.
To solve this problem you must compare the same columns but from different tables or different columns from the same table.
Query2:
CREATE OR REPLACE FUNCTION check (npi TEXT)
RETURNS BOOLEAN AS $$
BEGIN
SELECT 1
FROM org doc
WHERE doc.npi = pass_npi
;
RETURN 1;
END $$

Function to select existing value or insert new row

I'm new to working with PL/pgSQL, and I'm attempting to create a function that will either find the ID of an existing row, or will insert a new row if it is not found, and return the new ID.
The query contained in the function below works fine on its own, and the function gets created fine. However, when I try to run it, I get an error stating "ERROR: column reference "id" is ambiguous". Can anybody identify my problem, or suggest a more appropriate way to do this?
create or replace function sp_get_insert_company(
in company_name varchar(100)
)
returns table (id int)
as $$
begin
with s as (
select
id
from
companies
where name = company_name
),
i as (
insert into companies (name)
select company_name
where not exists (select 1 from s)
returning id
)
select id
from i
union all
select id
from s;
end;
$$ language plpgsql;
This is how I call the function:
select sp_get_insert_company('TEST')
And this is the error that I get:
SQL Error [42702]: ERROR: column reference "id" is ambiguous
Detail: It could refer to either a PL/pgSQL variable or a table column.
Where: PL/pgSQL function sp_get_insert_company(character varying) line 3 at SQL statement
As the messages says, id is in there twice. Once in the queries, once in the table definition of the return type. Somehow this clashes.
Try qualifying the column expressions, everywhere.
...
with s as (
select
companies.id
from
companies
where name = company_name
),
i as (
insert into companies (name)
select company_name
where not exists (select 1 from s)
returning companies.id
)
select i.id
from i
union all
select s.id
from s;
...
By qualifying the column expression the DBMS does no longer confuse id of a table with the id in the return type definition.
The next problem will be, that your SELECT has no target. It will tell you to do a PERFORM instead. But I assume you want to return the results. Change the body to
...
RETURN QUERY (
with s as (
select
companies.id
from
companies
where name = company_name
),
i as (
insert into companies (name)
select company_name
where not exists (select 1 from s)
returning companies.id
)
select i.id
from i
union all
select s.id
from s);
...
to do so.
In the function you display there is no need for returns table (id int). It's supposed to always return exactly one integer ID. Simplify to RETURNS int. This also makes ERROR: column reference "id" is ambiguous go away, since we implicitly removed the OUT parameter id (visible in the whole function block).
How to return result of a SELECT inside a function in PostgreSQL?
There is also no need for LANGUAGE plpgsql. Could simply be LANGUAGE sql, then you wouldn't need to add RETURNS QUERY, either. So:
CREATE OR REPLACE FUNCTION sp_get_insert_company(_company_name text)
RETURNS int AS
$func$
WITH s as (
select c.id -- still good style to table-qualify all columns
from companies c
where c.name = _company_name
),
i as (
insert into companies (name)
select _company_name
where not exists (select 1 from s)
returning id
)
select s.id from s
union all
select i.id from i
LIMIT 1; -- to optimize performance
$func$ LANGUAGE sql;
Except that it still suffers from concurrency issues. Find a proper solution for your undisclosed version of Postgres in this closely related answer:
Is SELECT or INSERT in a function prone to race conditions?

Same queries in PostgreSQL stored procedure

So, I'm trying to create a procedure that is going to find
a specific row in my table, save the row in a result to be
returned, delete the row and afterwards return the result.
The best thing I managed to do was the following:
CREATE OR REPLACE FUNCTION sth(foo integer)
RETURNS TABLE(a integer, b integer, ... other fields) AS $$
DECLARE
to_delete_id integer;
BEGIN
SELECT id INTO to_delete_id FROM my_table WHERE sth_id = foo LIMIT 1;
RETURN QUERY SELECT * FROM my_table WHERE sth_id = foo LIMIT 1;
DELETE FROM my_table where id = to_delete_id;
END;
$$ LANGUAGE plpgsql;
As you see, I have 2 SELECT operations that pretty much do the same thing (extra
overhead). Is there a way to just have the second SELECT and also set the to_delete_id
so I can delete the row afterwards?
You just want a DELETE...RETURNING.
DELETE FROM my_table WHERE sth_id=foo LIMIT 1 RETURNING *
Edit based on ahwnn's comment. Quite right too - teach me to cut + paste the query without reading it properly.
DELETE FROM my_table WHERE id = (SELECT id ... LIMIT 1) RETURNING *
Can be done much easier:
CREATE OR REPLACE FUNCTION sth(foo integer)
RETURNS SETOF my_table
AS
$$
BEGIN
return query
DELETE FROM my_table p
where sth_id = foo
returning *;
END;
$$
LANGUAGE plpgsql;
Select all the columns into variables, return them, then delete using the id:
Declare a variables for each column (named by convention the save as the column but with a leading underscore), then:
SELECT id, col1, col2, ...
INTO _id, _col1, _col22, ...
FROM my_table
WHERE sth_id = foo
LIMIT 1;
RETURN QUERY SELECT _id, _col1, _col22, ...;
DELETE FROM my_table where id = _id;

How can I perform an AND on an unknown number of booleans in postgresql?

I have a table with a foreign key and a boolean value (and a bunch of other columns that aren't relevant here), as such:
CREATE TABLE myTable
(
someKey integer,
someBool boolean
);
insert into myTable values (1, 't'),(1, 't'),(2, 'f'),(2, 't');
Each someKey could have 0 or more entries. For any given someKey, I need to know if a) all the entries are true, or b) any of the entries are false (basically an AND).
I've come up with the following function:
CREATE FUNCTION do_and(int4) RETURNS boolean AS
$func$
declare
rec record;
retVal boolean = 't'; -- necessary, or true is returned as null (it's weird)
begin
if not exists (select someKey from myTable where someKey = $1) then
return null; -- and because we had to initialise retVal, if no rows are found true would be returned
end if;
for rec in select someBool from myTable where someKey = $1 loop
retVal := rec.someBool AND retVal;
end loop;
return retVal;
end;
$func$ LANGUAGE 'plpgsql' VOLATILE;
... which gives the correct results:
select do_and(1) => t
select do_and(2) => f
select do_and(3) => null
I'm wondering if there's a nicer way to do this. It doesn't look too bad in this simple scenario, but once you include all the supporting code it gets lengthier than I'd like. I had a look at casting the someBool column to an array and using the ALL construct, but I couldn't get it working... any ideas?
No need to redefine functions PostgreSQL already provides: bool_and() will do the job:
select bool_and(someBool)
from myTable
where someKey = $1
group by someKey;
(Sorry, can't test it now)
Similar to the previous one, but in one query, this will do the trick, however, it is not clean nor easily-understandable code:
SELECT someKey,
CASE WHEN sum(CASE WHEN someBool THEN 1 ELSE 0 END) = count(*)
THEN true
ELSE false END as boolResult
FROM table
GROUP BY someKey
This will get all the responses at once, if you only want one key just add a WHERE clause
I just installed PostgreSQL for the first time this week, so you'll need to clean up the syntax, but the general idea here should work:
return_value = NULL
IF EXISTS
(
SELECT
*
FROM
My_Table
WHERE
some_key = $1
)
BEGIN
IF EXISTS
(
SELECT
*
FROM
My_Table
WHERE
some_key = $1 AND
some_bool = 'f'
)
SELECT return_value = 'f'
ELSE
SELECT return_value = 't'
END
The idea is that you only need to look at one row to see if any exist and if at least one row exists you then only need to look until you find a false value to determine that the final value is false (or you get to the end and it's true). Assuming that you have an index on some_key, performance should be good I would think.
(Very minor side-point: I think your function should be declared STABLE rather than VOLATILE, since it just uses data from the database to determine its result.)
As someone mentioned, you can stop scanning as soon as you encounter a "false" value. If that's a common case, you can use a cursor to actually provoke a "fast finish":
CREATE FUNCTION do_and(key int) RETURNS boolean
STABLE LANGUAGE 'plpgsql' AS $$
DECLARE
v_selector CURSOR(cv_key int) FOR
SELECT someBool FROM myTable WHERE someKey = cv_key;
v_result boolean;
v_next boolean;
BEGIN
OPEN v_selector(key);
LOOP
FETCH v_selector INTO v_next;
IF not FOUND THEN
EXIT;
END IF;
IF v_next = false THEN
v_result := false;
EXIT;
END IF;
v_result := true;
END LOOP;
CLOSE v_selector;
RETURN v_result;
END
$$;
This approach also means that you are only doing a single scan on myTable. Mind you, I suspect you need loads and loads of rows in order for the difference to be appreciable.
You can also use every, which is just an alias to bool_and:
select every(someBool)
from myTable
where someKey = $1
group by someKey;
Using every makes your query more readable. An example, show all persons who just eat apple every day:
select personId
from personDailyDiet
group by personId
having every(fruit = 'apple');
every is semantically the same as bool_and, but it's certainly clear that every is more readable than bool_and:
select personId
from personDailyDiet
group by personId
having bool_and(fruit = 'apple');
Maybe count 'all' items with somekey=somevalue and use it in a boolean comparison with the count of all 'True' occurences for somekey?
Some non-tested pseudo-sql to show what i mean...
select foo1.count_key_items = foo2.count_key_true_items
from
(select count(someBool) as count_all_items from myTable where someKey = '1') as foo1,
(select count(someBool) as count_key_true_items from myTable where someKey = '1' and someBool) as foo2
CREATE FUNCTION do_and(int4)
RETURNS boolean AS
$BODY$
SELECT
MAX(bar)::bool
FROM (
SELECT
someKey,
MIN(someBool::int) AS bar
FROM
myTable
WHERE
someKey=$1
GROUP BY
someKey
UNION
SELECT
$1,
NULL
) AS foo;
$BODY$
LANGUAGE 'sql' STABLE;
In case you don't need the NULL value (when there aren't any rows), simply use the query below:
SELECT
someKey,
MIN(someBool::int)::bool AS bar
FROM
myTable
WHERE
someKey=$1
GROUP BY
someKey
SELECT DISTINCT ON (someKey) someKey, someBool
FROM myTable m
ORDER BY
someKey, someBool NULLS FIRST
This will select the first ordered boolean value for each someKey.
If there is a single FALSE or a NULL, it will be returned first, meaning that the AND failed.
If the first boolean is a TRUE, then all other booleans are also TRUE for this key.
Unlike the aggregate, this will use the index on (someKey, someBool).
To return an OR, just reverse the ordering:
SELECT DISTINCT ON (someKey) someKey, someBool
FROM myTable m
ORDER BY
someKey, someBool DESC NULLS FIRST