Filter table in a function using a table valued parameter - sql

I have a table-valued function that uses a parameter to filter a table and returns the results:
drop table if exists mytbl;
create table mytbl (i int);
insert into mytbl (i) values (1),(2),(3);
CREATE FUNCTION filterer(
_filter int
)
RETURNS TABLE(i_ret int)
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
RETURN QUERY SELECT i
FROM mytbl
WHERE i = _filter;
END;
$BODY$;
select * from filterer(1);
it returns:
So far so good. However what I really want to do is filter on a list of values rather than just one. I figured the way to do it is create a table-valued parameter using a type that I define using CREATE TYPE, but I can't get it working. Here's what I have so far:
drop table if exists mytbl;
create table mytbl (i int);
DO $$
BEGIN
IF NOT EXISTS (SELECT * FROM pg_type t WHERE t.typname = 'tp') THEN
CREATE TYPE tp AS (i int);
END IF;
END$$;
insert into mytbl (i) values (1),(2),(3);
DROP FUNCTION public.filterer(tp);
CREATE FUNCTION filterer(
_filter tp
)
RETURNS TABLE(i_ret int)
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
RETURN QUERY SELECT i
FROM mytbl
WHERE i IN (_filter);
END;
$BODY$;
select * from filterer(1);
When I run that code it fails with:
ERROR: function filterer(integer) does not exist LINE 23: select *
from filterer(1);
How do I declare a variable of type tp and pass it into my function?

working example
Two things here:
WHERE i IN (_filter);
should be replaced with
WHERE row(i) IN (_filter);
and calling:
select * from filterer(row(1));
I assume the question is purely academic, as I can't see any use of such type?..
CREATE TYPE tp AS (i int);

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$;

PostgreSQL - Shared temp table between functions

I wnat to know if its possible to share a temporary table between functions that are called in a "main function", like this:
-- some sub function
create or replace function up_sub_function (str text)
returns table (id int, descr text) as $$
begin
return query select * from temp_table where descr like concat('%', str , '%');
end; $$
language plpgsql;
-- main function
create or replace function up_main_function ()
returns table (id int, descr text) as $$
begin
create temporary table temp_table if not exists (
id int,
descr text
);
insert into temp_campaigns select id, descr from test_table;
return query select * from up_sub_function('a');
end; $$
language plpgsql;
BEGIN;
select * from up_main_function();
drop table temp_table;
COMMIT;
If you can show me the correct way to achieve this, I want to be able to populate a temporary table and then filter rows by calling othe functions inside the main function.
Thanks ans happy programming! :)
See the documentation https://www.postgresql.org/docs/current/static/sql-createtable.html
temp tables are valid for the entire session. That is as long as you stay connected to the database.
In your case you only need it during the transaction. So you should create it with ON COMMIT DROP
create temporary table temp_table if not exists (
id int,
descr text
) ON COMMIT DROP;
Once you created the table you can use it within any function in the current transaction.
You do not need the BEGIN to start the transaction. A transaction is automatically started when the outer function is called.
Nested function calls share the same transaction. So they all see the table.

Temporary table postgresql function

I can't find a clear explanation of the syntax to create (and use) tables just for the inside calculations of a function. Could anyone give me a syntax exemple please ?
From what I've found, I have tried this (with and without # before temp_table) :
CREATE FUNCTION test.myfunction()
RETURNS SETOF test.out_table
AS $$
DECLARE #temp_table TABLE
(
id int,
value text
)
BEGIN
INSERT INTO #temp_table
SELECT id, value
FROM test.another_table;
INSERT INTO test.out_table
SELECT id, value
FROM #temp_table;
RETURN END
$$ LANGUAGE SQL;
I get :
ERROR: syntax error at or near "DECLARE"
LINE 5: DECLARE #temp_table TABLE
-
I also tried the CREATE TABLE approach suggested here, this way :
CREATE FUNCTION test.myfunction()
RETURNS SETOF test.out_table
AS $$
CREATE TABLE temp_table AS
SELECT id, value
FROM test.another_table;
INSERT INTO test.out_table
SELECT id, value
FROM temp_table;
$$ LANGUAGE SQL;
And I get this :
ERROR: relation "temp_table " does not exist
LINE 11: FROM temp_table
(Obviously, I'm aware the temp_table is not necessary for what I'm doing in the code above, but that's not the point :) => I want to understand the syntax to get it to work)
The appropriate syntax for creating a temp table is
create temp table...
but you have to be sure to drop the temp table before existing out of the function. Also, I'd suggest this syntax instead:
CREATE TEMP TABLE IF NOT EXISTS temp_table AS
SELECT id, value
FROM test.another_table;
Thus your function will be like this:
CREATE FUNCTION test.myfunction()
RETURNS SETOF test.out_table
AS $$
CREATE TEMP TABLE IF NOT EXISTS temp_table AS
SELECT id, value
FROM test.another_table;
INSERT INTO test.out_table
SELECT id, value
FROM temp_table;
DROP TABLE temp_table;
$$ LANGUAGE SQL;
But if I can be so kind, I'd like to rewrite this function so it is more correct:
CREATE FUNCTION test.myfunction()
RETURNS TABLE (id int, value varchar) -- change your datatype as needed
AS $$
BEGIN;
CREATE TEMP TABLE IF NOT EXISTS temp_table AS
SELECT id, value
FROM test.another_table;
INSERT INTO test.out_table
SELECT id, value
FROM temp_table;
DROP TABLE temp_table;
RETURN QUERY
SELECT id, value
from temp_table;
END;
$$ LANGUAGE plpgsql;
Untested; let me know if this fails.

How to create SQL function with parameter?

I have an SQL function:
-- Function: insert_into_wgs()
-- DROP FUNCTION insert_into_wgs();
CREATE OR REPLACE FUNCTION insert_into_wgs()
RETURNS void AS
$BODY$
BEGIN
INSERT INTO parcels (num,vid,support_num,name_dispatcher,cadastr,the_geom,status_id)
SELECT num,vid,support_num,name_dispatcher,cadastr,ST_TRANSFORM(the_geom,4326),status_id
FROM parcels_temp
WHERE num NOT IN (SELECT num FROM parcels)AND deleted='no';
end;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION insert_into_wgs() OWNER TO postgres;
You see that it works only for parcels, parcels_temp tables. How can I create function with parameters insert_into_wgs(table, table_temp);?
As mentioned in the comment, you have to use dynamic SQL if you want to parametrize identifiers. And use EXECUTE in plpgsql.
In the base query better use NOT EXISTS instead of col NOT IN (<subquery>).
Now, if only the table names change and the columns stay the same, this simple demo would do the job:
CREATE OR REPLACE FUNCTION insert_into_tbl(_tbl text, _tbl_tmp text)
RETURNS void AS
$func$
BEGIN
EXECUTE format($x$
INSERT INTO %I (...) -- enter columns
SELECT ... -- enter columns
FROM %I t
WHERE NOT EXISTS (
SELECT 1
FROM %1$I p -- reuse first table name
WHERE p.num = t.num
)
AND deleted = 'no'$x$, _tbl, _tbl_tmp);
END
$func$ LANGUAGE plpgsql;
Check out format() in the manual.
There are quite a few related answers on SO. Try this search.