Same queries in PostgreSQL stored procedure - sql

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;

Related

Dynamic query that uses CTE gets "syntax error at end of input"

I have a table that looks like this:
CREATE TABLE label (
hid UUID PRIMARY KEY DEFAULT UUID_GENERATE_V4(),
name TEXT NOT NULL UNIQUE
);
I want to create a function that takes a list of names and inserts multiple rows into the table, ignoring duplicate names, and returns an array of the IDs generated for the rows it inserted.
This works:
CREATE OR REPLACE FUNCTION insert_label(nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
WITH new_names AS (
INSERT INTO label(name)
SELECT tn.name
FROM tmp_names tn
WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name)
RETURNING hid
)
SELECT ARRAY_AGG(hid) INTO ids
FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
I have many tables with the exact same columns as the label table, so I would like to have a function that can insert into any of them. I'd like to create a dynamic query to do that. I tried that, but this does not work:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('WITH new_names AS ( INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid)', h_tbl);
EXECUTE query_str;
SELECT ARRAY_AGG(hid) INTO ids FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
This is the output I get when I run that function:
psql=# select insert_label('label', array['how', 'now', 'brown', 'cow']);
ERROR: syntax error at end of input
LINE 1: ...SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
^
QUERY: WITH new_names AS ( INSERT INTO label(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
CONTEXT: PL/pgSQL function insert_label(regclass,text[]) line 19 at EXECUTE
The query generated by the dynamic SQL looks like it should be exactly the same as the query from static SQL.
I got the function to work by changing the return value from an array of UUIDs to a table of UUIDs and not using CTE:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS TABLE (hid UUID)
AS $$
DECLARE
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid', h_tbl);
RETURN QUERY EXECUTE query_str;
DROP TABLE tmp_names;
RETURN;
END;
$$ LANGUAGE PLPGSQL;
I don't know if one way is better than the other, returning an array of UUIDs or a table of UUIDs, but at least I got it to work one of those ways. Plus, possibly not using a CTE is more efficient, so it may be better to stick with the version that returns a table of UUIDs.
What I would like to know is why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
If anyone can let me know what I did wrong, I would appreciate it.
... why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
No, it was only the CTE without (required) outer query. (You had SELECT ARRAY_AGG(hid) INTO ids FROM new_names in the static version.)
There are more problems, but just use this query instead:
INSERT INTO label(name)
SELECT unnest(nms)
ON CONFLICT DO NOTHING
RETURNING hid;
label.name is defined UNIQUE NOT NULL, so this simple UPSERT can replace your function insert_label() completely.
It's much simpler and faster. It also defends against possible duplicates from within your input array that you didn't cover, yet. And it's safe under concurrent write load - as opposed to your original, which might run into race conditions. Related:
How to use RETURNING with ON CONFLICT in PostgreSQL?
I would just use the simple query and replace the table name.
But if you still want a dynamic function:
CREATE OR REPLACE FUNCTION insert_label(_tbl regclass, _nms text[])
RETURNS TABLE (hid uuid)
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE format(
$$
INSERT INTO %s(name)
SELECT unnest($1)
ON CONFLICT DO NOTHING
RETURNING hid
$$, _tbl)
USING _nms;
END
$func$;
If you don't need an array as result, stick with the set (RETURNS TABLE ...). Simpler.
Pass values (_nms) to EXECUTE in a USING clause.
The tablename (_tbl) is type regclass, so the format specifier %I for format() would be wrong. Use %s instead. See:
Table name as a PostgreSQL function parameter

How to add column inside postgres function without saving it to the db?

I have a postgres DB and I want to create a function that returns a copy of my table with a new column that has a value of 1 if its id is inside the array(idds[]) that the function gets as an input.
In the code below I've try to create a temporary table(chosen) that have id if it's in the idds array and to manually add the isChosen column that obviously doesn't work...
CREATE OR REPLACE FUNCTION public.getTableFromArray(idds integer[])
RETURNS table(
id INTEGER,
isChosen INTEGER
)
LANGUAGE 'plpgsql'
AS $BODY$
begin
with chosen AS(SELECT id,isChosen=1 FROM table1 WHERE ARRAY[table1.id] <# idds)
return query
SELECT id FROM table1 LEFT JOIN chosen ON table1.id=chosen.id;
end;
$BODY$;
Or, with a lot less noise, a proper boolean output column, and without the unhelpful CaMeL case identifiers in a plain SQL function:
CREATE OR REPLACE FUNCTION public.get_table_from_array(idds integer[])
RETURNS TABLE(id int, is_chosen bool)
LANGUAGE sql AS
'SELECT t.id, t.id = ANY(idds) FROM table1 t';
Might as well just run the SQL command directly, though:
SELECT id, id = ANY('{1,2,3}'::int[]) AS is_chosen FROM table1;
you can use this query instead :
select * , case when ARRAY[table1.id] <# idds then 1 else 0 end as choosen FROM table1;
so:
CREATE OR REPLACE FUNCTION public.getTableFromArray(idds integer[])
RETURNS table(
id INTEGER,
isChosen INTEGER
)
LANGUAGE 'plpgsql'
AS $BODY$
begin
return query
select id , case when ARRAY[table1.id] <# idds then 1 else 0 end as isChosen FROM table1;
end;
$BODY$;

Return setof record with 1 row

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.

PostgreSQL ORDER BY values in IN() clause

Ok, there are some answers out there on how to do this. But all of the answers are assuming that the query is selecting all. If you have a distinct select, the methods no longer work.
See here for that method: Simulating MySQL's ORDER BY FIELD() in Postgresql
Basically I have
SELECT DISTINCT id
FROM items
WHERE id IN (5,2,9)
ORDER BY
CASE id
WHEN 5 THEN 1
WHEN 2 THEN 2
WHEN 9 THEN 3
END
Of course, this breaks and says
"PGError: ERROR: for SELECT DISTINCT, ORDER BY expressions must
appear in select list"
Is there any way to order your query results in PostgreSQL by the order of the values in the IN clause?
You can wrap it into a derived table:
SELECT *
FROM (
SELECT DISTINCT id
FROM items
WHERE id IN (5,2,9)
) t
ORDER BY
CASE id
WHEN 5 THEN 1
WHEN 2 THEN 2
WHEN 9 THEN 3
END
From documentation:
Tip: Grouping without aggregate expressions effectively calculates the
set of distinct values in a column. This can also be achieved using
the DISTINCT clause (see Section 7.3.3).
SQL query:
SELECT id
FROM items
WHERE id IN (5,2,9)
GROUP BY id
ORDER BY
CASE id
WHEN 5 THEN 1
WHEN 2 THEN 2
WHEN 9 THEN 3
END;
I create this function in postgres PL/PGSQL and it is a lot easier to use.
-- Function: uniqueseperategeomarray(geometry[], double precision)
-- DROP FUNCTION uniqueseperategeomarray(geometry[], double precision);
CREATE OR REPLACE FUNCTION manualidsort(input_id int, sort_array int[])
RETURNS int AS
$BODY$
DECLARE
input_index int;
each_item int;
index int;
BEGIN
index := 1;
FOREACH each_item IN ARRAY sort_array
LOOP
IF each_item = input_id THEN
RETURN index;
END IF;
index := index+1;
END LOOP;
END;
$BODY$
LANGUAGE plpgsql;
ALTER FUNCTION manualidsort(int, int[])
OWNER TO staff;
And when you run a query, go like this...
SELECT * FROM my_table ORDER BY manualidsort(my_table_id, ARRAY[25, 66, 54])
Looking around I couldn't find a complete solution to this question.
I think the better solution is to use this query
SELECT *
FROM items
WHERE id IN (5,2,9)
ORDER BY idx(array[5,2,9]::integer[], items.id)
If you are using PostgreSQL >= 9.2 you can enable the idx function enabling the extension.
CREATE EXTENSION intarray;
Otherwise you can create it with the following:
CREATE OR REPLACE FUNCTION idx(anyarray, anyelement)
RETURNS INT AS
$$
SELECT i FROM (
SELECT generate_series(array_lower($1,1),array_upper($1,1))
) g(i)
WHERE $1[i] = $2
LIMIT 1;
$$ LANGUAGE SQL IMMUTABLE;
I really suggest to use ::integer[] in the query because if you are creating the array dynamically is possible that it has no elements resulting in ids(array[], items.id) which would raise an exception on PostgreSQL.

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