Using variables in a plpgsql function - postgresql

Ok so I used a string_agg like this.
select string_agg(DISTINCT first_name,', ' ORDER BY first_name) FROM person_test;
Then I wrote this to return the values to a table.
SELECT *
FROM person_test
where first_name = ANY(string_to_array('Aaron,Anne', ','));
Now I want to put this in a function so that instead of acturally putting names into the string_to_array, I can just call the string_agg.
I am new to postgres and am not finding any good documentation on how to do this online. I believe I would have to declare the the string_agg and then call it in string_to_array but I am having no such luck.
This was my attempt, I know this is now right but if anyone could add some feedback. I am getting an error between results and ALAIS and on the return.
create or REPLACE FUNCTION select_persons(VARIADIC names TEXT[]);
declare results ALIAS select string_agg(DISTINCT first_name,', ' ORDER BY first_name) FROM person_test;
BEGIN
return setof person_test LANGUAGE sql as $$
select * from person_test
where first_name = any(results)
end;
$$ language sql;

You can create a function with variable number of arguments.
Example:
create table person_test (id int, first_name text);
insert into person_test values
(1, 'Ann'), (2, 'Bob'), (3, 'Ben');
create or replace function select_persons(variadic names text[])
returns setof person_test language sql as $$
select *
from person_test
where first_name = any(names)
$$;
select * from select_persons('Ann');
id | first_name
----+------------
1 | Ann
(1 row)
select * from select_persons('Ann', 'Ben', 'Bob');
id | first_name
----+------------
1 | Ann
2 | Bob
3 | Ben
(3 rows)
To use a variable inside a plpgsql function, you should declare the variable and use select ... into (or assignment statement). Example:
create or replace function my_func()
returns setof person_test
language plpgsql as $$
declare
aggregated_names text;
begin
select string_agg(distinct first_name,', ' order by first_name)
into aggregated_names
from person_test;
-- here you can do something using aggregated_names
return query
select *
from person_test
where first_name = any(string_to_array(aggregated_names, ', '));
end $$;
select * from my_func();

Related

Array error passing dynamic number of parameters to function

I'm trying to create a function to receive the name of the table in my schema already created and a several name of columns within this table (dynamic number of columns) and return a table with all the columns in a unique column with the value of each column separated by comma.
I'm trying this:
CREATE OR REPLACE PROCEDURE public.matching(IN table text, VARIADIC column_names text[])
LANGUAGE 'plpgsql'
AS $BODY$DECLARE
column_text text;
BEGIN
EXECUTE format ($$ SELECT array_to_string(%s, ' ')$$, column_names) into column_text;
EXECUTE format ($$ CREATE TABLE temp1 AS
SELECT concat(%s, ' ') FROM %s $$, column_text, table);
END;$BODY$;
This return an error:
ERROR: syntax error at or near «{»
LINE 1: SELECT array_to_string({city,address}, ' ')
which is the error?
If you simplify the generation of the dynamic SQL, things get easier:
CREATE OR REPLACE PROCEDURE public.matching(IN table_name text, VARIADIC column_names text[])
LANGUAGE plpgsql
AS
$BODY$
DECLARE
l_sql text;
BEGIN
l_sql := format($s$
create table temp1 as
select concat_ws(',', %s) as everything
from %I
$s$, array_to_string(column_names, ','), table_name);
raise notice 'Running %', l_sql;
EXECUTE l_sql;
END;
$BODY$;
So if you e.g. pass in 'some_table' and {'one', 'two', 'three'} the generated SQL will look like this:
create table temp1 as select concat_ws(',', one,two,three) as everything from some_table
I also used a column alias for the new column, so that the new table has a defined name. Note that the way I put the column names into the SQL string won't properly deal with identifiers that need double quotes (but they should be avoided anyway)
If you want to "return a table", then maybe a function might be the better solution:
CREATE OR REPLACE function matching(IN table_name text, VARIADIC column_names text[])
returns table (everything text)
LANGUAGE plpgsql
AS
$BODY$
DECLARE
l_sql text;
BEGIN
l_sql := format($s$
select concat_ws(',', %s) as everything
from %I
$s$, array_to_string(column_names, ','), table_name);
return query execute l_sql;
END;
$BODY$;
Then you can use it like this:
select *
from matching('some_table', 'one', 'two', 'three');
I propose different but similar code.
With following script:
CREATE OR REPLACE PROCEDURE public.test(IN p_old_table text, IN p_old_column_names text[], IN p_new_table text)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
old_column_list text;
ctas_stmt text;
BEGIN
old_column_list = array_to_string(p_old_column_names,',');
RAISE NOTICE 'old_column_list=%', old_column_list;
ctas_stmt = format('CREATE TABLE %s AS SELECT %s from %s', p_new_table, old_column_list, p_old_table);
RAISE NOTICE 'ctas_stmt=%', ctas_stmt;
EXECUTE ctas_stmt;
END;
$BODY$;
--
create table t(x int, y text, z timestamp, z1 text);
insert into t values (1, 'OK', current_timestamp, null);
select * from t;
--
call test('t',ARRAY['x','y','z'], 'tmp');
--
\d tmp;
select * from tmp;
I have following execution:
CREATE OR REPLACE PROCEDURE public.test(IN p_old_table text, IN p_old_column_names text[], IN p_new_table text)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
old_column_list text;
ctas_stmt text;
BEGIN
old_column_list = array_to_string(p_old_column_names,',');
RAISE NOTICE 'old_column_list=%', old_column_list;
ctas_stmt = format('CREATE TABLE %s AS SELECT %s from %s', p_new_table, old_column_list, p_old_table);
RAISE NOTICE 'ctas_stmt=%', ctas_stmt;
EXECUTE ctas_stmt;
END;
$BODY$;
CREATE PROCEDURE
create table t(x int, y text, z timestamp, z1 text);
CREATE TABLE
insert into t values (1, 'OK', current_timestamp, null);
INSERT 0 1
select * from t;
x | y | z | z1
---+----+----------------------------+----
1 | OK | 2020-04-14 11:37:28.641328 |
(1 row)
call test('t',ARRAY['x','y','z'], 'tmp');
psql:tvar.sql:24: NOTICE: old_column_list=x,y,z
psql:tvar.sql:24: NOTICE: ctas_stmt=CREATE TABLE tmp AS SELECT x,y,z from t
CALL
Table "public.tmp"
Column | Type | Collation | Nullable | Default
--------+-----------------------------+-----------+----------+---------
x | integer | | |
y | text | | |
z | timestamp without time zone | | |
select * from tmp;
x | y | z
---+----+----------------------------
1 | OK | 2020-04-14 11:37:28.641328
(1 row)

How to execute an (expression involving columns of the same table) stored in another column of the table?

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

plpgsql - selecting array of multiple columns

I am trying to create procedure which selects data, processes and returns them, but I am struggling how to define array variable for multiple columns.
This works:
CREATE OR REPLACE FUNCTION testing_array_return()
RETURNS TABLE(id BIGINT) AS
$body$
DECLARE
l_rows BIGINT[];
BEGIN
-- select data using for update etc
l_rows := ARRAY(
SELECT 1 AS id
UNION
SELECT 2 AS id
);
-- do some stuff
-- return previously selected data
RETURN QUERY
SELECT *
FROM UNNEST(l_rows);
END;
$body$
LANGUAGE 'plpgsql'
But I want to do this for 2 or more columns without using composite type or rowtype:
CREATE OR REPLACE FUNCTION testing_array_return()
RETURNS TABLE(id BIGINT, text VARCHAR2) AS
$body$
DECLARE
l_rows -- what should I put here?
BEGIN
-- select data using for update etc
l_rows := ARRAY(
SELECT 1 AS id, 'test' AS text
UNION
SELECT 2 AS id, 'test2' AS text
);
-- do some stuff
-- return previously selected data
RETURN QUERY
SELECT *
FROM UNNEST(l_rows);
END;
$body$
LANGUAGE 'plpgsql'
In oracle I could define record type and then table type of it, but I can't find how to do this in postgres. Maybe using wrong keywords when searching...
edit: this is how I would do this in Oracle (without returning).
DECLARE
TYPE t_row IS RECORD(
id NUMBER
,text VARCHAR2(10));
TYPE t_tbl IS TABLE OF t_row;
l_rows t_tbl := t_tbl(); --how to do this in postgres?
BEGIN
SELECT *
BULK COLLECT
INTO l_rows
FROM (SELECT 1 AS id
,'test' AS text
FROM dual
UNION ALL
SELECT 2 AS id
,'test' AS text
FROM dual);
END;
Anything similiar in postgres? Like record but for array.
You would do the same in Postgres:
Create the record type:
create type footype as (id bigint, text_ varchar);
Then use an array of that type in your function:
CREATE OR REPLACE FUNCTION testing_array_return()
RETURNS TABLE(id BIGINT, text VARCHAR) AS
$body$
DECLARE
l_rows footype[];
BEGIN
-- select data using for update etc
l_rows := ARRAY(
SELECT (1, 'test')
UNION
SELECT (2, 'test2')
);
-- do some stuff
-- return previously selected data
RETURN QUERY
SELECT *
FROM UNNEST(l_rows);
END;
$body$
LANGUAGE plpgsql;
Online example: http://rextester.com/UMRZFO54266
The expression (1, 'test') creates a single value of record type. As that is assigned to a typed variable, there is no need to alias the columns (and in fact you can't do that anyway).
Unrelated, but: the language name is an identifier. Do not put that in single quotes.
Note that text is also a keyword in Postgres because it's a data type. You shouldn't use it as a column name

Get IDs from multiple columns in multiple tables as one set or array

I have multiple tables with each two rows of interest: connection_node_start_id and connection_node_end_id. My goal is to get a collection of all those IDs, either as a flat ARRAY or as a new TABLE consisting of one row.
Example output ARRAY:
result = {1,4,7,9,2,5}
Example output TABLE:
IDS
-------
1
4
7
9
2
5
My fist attempt is somewhat clumsy and does not work properly as the SELECT statement just returns one row. It seems there must be a simple way to do this, can someone point me into the right direction?
CREATE OR REPLACE FUNCTION get_connection_nodes(anyarray)
RETURNS anyarray AS
$$
DECLARE
table_name varchar;
result integer[];
sel integer[];
BEGIN
FOREACH table_name IN ARRAY $1
LOOP
RAISE NOTICE 'table_name(%)',table_name;
EXECUTE 'SELECT ARRAY[connection_node_end_id,
connection_node_start_id] FROM ' || table_name INTO sel;
RAISE NOTICE 'sel(%)',sel;
result := array_cat(result, sel);
END LOOP;
RETURN result;
END
$$
LANGUAGE 'plpgsql';
Test table:
connection_node_start_id | connection_node_end_id
--------------------------------------------------
1 | 4
7 | 9
Call:
SELECT get_connection_nodes(ARRAY['test_table']);
Result:
{1,4} -- only 1st row, rest is missing
For Postgres 9.3+
CREATE OR REPLACE FUNCTION get_connection_nodes(text[])
RETURNS TABLE (ids int) AS
$func$
DECLARE
_tbl text;
BEGIN
FOREACH _tbl IN ARRAY $1
LOOP
RETURN QUERY EXECUTE format('
SELECT t.id
FROM %I, LATERAL (VALUES (connection_node_start_id)
, (connection_node_end_id)) t(id)'
, _tbl);
END LOOP;
END
$func$ LANGUAGE plpgsql;
Related answer on dba.SE:
SELECT DISTINCT on multiple columns
Or drop the loop and concatenate a single query. Probably fastest:
CREATE OR REPLACE FUNCTION get_connection_nodes2(text[])
RETURNS TABLE (ids int) AS
$func$
BEGIN
RETURN QUERY EXECUTE (
SELECT string_agg(format(
'SELECT t.id FROM %I, LATERAL (VALUES (connection_node_start_id)
, (connection_node_end_id)) t(id)'
, tbl), ' UNION ALL ')
FROM unnest($1) tbl
);
END
$func$ LANGUAGE plpgsql;
Related:
Loop through like tables in a schema
LATERAL was introduced with Postgres 9.3.
For older Postgres
You can use the set-returning function unnest() in the SELECT list, too:
CREATE OR REPLACE FUNCTION get_connection_nodes2(text[])
RETURNS TABLE (ids int) AS
$func$
BEGIN
RETURN QUERY EXECUTE (
SELECT string_agg(
'SELECT unnest(ARRAY[connection_node_start_id
, connection_node_end_id]) FROM ' || tbl
, ' UNION ALL '
)
FROM (SELECT quote_ident(tbl) AS tbl FROM unnest($1) tbl) t
);
END
$func$ LANGUAGE plpgsql;
Should work with pg 8.4+ (or maybe even older). Works with current Postgres (9.4) as well, but LATERAL is much cleaner.
Or make it very simple:
CREATE OR REPLACE FUNCTION get_connection_nodes3(text[])
RETURNS TABLE (ids int) AS
$func$
BEGIN
RETURN QUERY EXECUTE (
SELECT string_agg(format(
'SELECT connection_node_start_id FROM %1$I
UNION ALL
SELECT connection_node_end_id FROM %1$I'
, tbl), ' UNION ALL ')
FROM unnest($1) tbl
);
END
$func$ LANGUAGE plpgsql;
format() was introduced with pg 9.1.
Might be a bit slower with big tables because each table is scanned once for every column (so 2 times here). Sort order in the result is different, too - but that does not seem to matter for you.
Be sure to sanitize escape identifiers to defend against SQL injection and other illegal syntax. Details:
Table name as a PostgreSQL function parameter
The EXECUTE ... INTO statement can only return data from a single row:
If multiple rows are returned, only the first will be assigned to the INTO variable.
In order to concatenate values from all rows you have to aggregate them first by column and then append the arrays:
EXECUTE 'SELECT array_agg(connection_node_end_id) ||
array_agg(connection_node_start_id) FROM ' || table_name INTO sel;
You're probably looking for something like this:
CREATE OR REPLACE FUNCTION d (tblname TEXT [])
RETURNS TABLE (c INTEGER) AS $$
DECLARE sql TEXT;
BEGIN
WITH x
AS (SELECT unnest(tblname) AS tbl),
y AS (
SELECT FORMAT('
SELECT connection_node_end_id
FROM %s
UNION ALL
SELECT connection_node_start_id
FROM %s
', tbl, tbl) AS s
FROM x)
SELECT string_agg(s, ' UNION ALL ')
INTO sql
FROM y;
RETURN QUERY EXECUTE sql;
END;$$
LANGUAGE plpgsql;
CREATE TABLE a (connection_node_end_id INTEGER, connection_node_start_id INTEGER);
INSERT INTO A VALUES (1,2);
CREATE TABLE b (connection_node_end_id INTEGER, connection_node_start_id INTEGER);
INSERT INTO B VALUES (100, 101);
SELECT * from d(array['a','b']);
c
-----
1
2
100
101
(4 rows)

Postgres dynamic sql and list result

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