Array error passing dynamic number of parameters to function - postgresql

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)

Related

Count for each columns in table (not null) PostgreSQL PL/pgSQL

I am trying to count the number of rows that do not contain null for each column in the table
There is a simple table actor_new
The first 2 columns (actor_id, first_name) contain 203 rows not null
Other 2 columns (last_name, last_update) contain 200 rows not null
This is a simple test that outputs the same value for all columns, but if you perform select separately, then everything works correctly, please help me understand the LOOP block
create or replace function new_cnt_test_ho(in_table text, out out_table text, out cnt_rows int) returns setof record AS $$
DECLARE i text;
BEGIN
FOR i IN
select column_name
from information_schema."columns"
where table_schema = 'public'
and table_name = in_table
LOOP
execute '
select $1, count($1)
from '|| quote_ident(in_table) ||'
where $1 is not null '
INTO out_table, cnt_rows
using i, quote_literal(i), quote_ident(in_table), quote_literal(in_table) ;
return next;
END LOOP;
END;
$$LANGUAGE plpgsql
Result:
select * from new_cnt_test_ho('actor_new')
out_table |cnt_rows|
-----------+--------+
actor_id | 203|
first_name | 203|
last_name | 203|
last_update| 203|
There are 4 parameters specified in using, because I assumed that the error was in quotes, I took turns playing with arguments from 1 to 4
The correct result should be like this
out_table |cnt_rows|
-----------+--------+
actor_id | 203|
first_name | 203|
last_name | 200|
last_update| 200|
based on your title: input is a table name, output is a table one column is column name, another column is return of count(column)
first check the table exists or not.
then for loop get each column name, after that for each column name run a query.
a sample query is select 'cola',count(cola) from count_nulls. first occurrence is literal 'cola', so we need quote_literal(cols.column_name),
second is the column name, so we need use quote_ident(cols.column_name)
select 'cola',count(cola) from count_nulls will count column cola all not null value. if a column all value is null then return 0.
The following function will return the expected result. Can be simplified, since i use a lot of raise notice.
CREATE OR REPLACE FUNCTION get_all_nulls (_table text)
RETURNS TABLE (
column_name_ text,
numberofnull bigint
)
AS $body$
DECLARE
cols RECORD;
_sql text;
_table_exists boolean;
_table_reg regclass;
BEGIN
_table_reg := _table::regclass;
_table_exists := (
SELECT
EXISTS (
SELECT
FROM
pg_tables
WHERE
schemaname = 'public'
AND tablename = _table));
FOR cols IN
SELECT
column_name
FROM
information_schema.columns
WHERE
table_name = _table
AND table_schema = 'public' LOOP
_sql := 'select ' || quote_literal(cols.column_name) || ',count(' || quote_ident(cols.column_name) || ') from ' || quote_ident(_table::text);
RAISE NOTICE '_sql:%', _sql;
RETURN query EXECUTE _sql;
END LOOP;
END;
$body$ STRICT
LANGUAGE plpgsql;
setup.
begin;
create table count_nulls(cola int, colb int, colc int);
INSERT into count_nulls values(null,null,null);
INSERT into count_nulls values(1,null,null);
INSERT into count_nulls values(2,3,null);
commit;

Postgresql [42P01] ERROR: relation does not exist during trigger function

I am trying to write a trigger that stores previous versions of a row in a table named audit_tablename given a table named tablename.
Here is the the code...
CREATE OR REPLACE FUNCTION process_ui_audit()
RETURNS TRIGGER AS
$$
DECLARE
audit_table_name text := TG_TABLE_SCHEMA || '.audit_' || TG_TABLE_NAME;
audit_table_schema text := TG_TABLE_SCHEMA;
BEGIN
IF (TG_OP = 'UPDATE')
THEN
EXECUTE FORMAT('INSERT INTO %1$I SELECT NEXTVAL(''$1.hibernate_sequence''),now(), user, ($1).*',
audit_table_name, audit_table_schema)
USING OLD;
NEW.version = OLD.version + 1;
RETURN NEW;
ELSIF (TG_OP = 'INSERT')
THEN
NEW.version = 1;
RETURN NEW;
END IF;
END;
When I try to update a row the trigger runs and I get errors like this....
[42P01] ERROR: relation "webapp.audit_portal_user" does not exist
Where: PL/pgSQL function webapp.process_ui_audit() line 13 at EXECUTE
I am wonderin am I formatting table names incorrectly or something? The table name webapp.audit_portal_user definetly exists.
It works without specifying schema name.
Here is a simplified example:
create table portal_user(
uid int,
uname text
);
CREATE TABLE
create table audit_portal_user(
uid int,
uname text,
who text,
what text,
ts timestamp
);
CREATE TABLE
create or replace function process_ui_audit()
returns trigger as
$$
declare
audit_table_name text := 'audit_' || tg_table_name;
begin
if (tg_op = 'UPDATE')
then
execute format('insert into %I values($1.*, user, %L, now())',
audit_table_name, 'UPDATE') using new;
return null;
end if;
end;
$$
language plpgsql;
CREATE FUNCTION
create trigger audit
after update on portal_user
for each row
execute function process_ui_audit();
CREATE TRIGGER
insert into portal_user values(12, 'titi');
INSERT 0 1
select * from portal_user;
uid | uname
-----+-------
12 | titi
(1 row)
update portal_user set uname='toto' where uid=12;
UPDATE 1
select * from portal_user;
uid | uname
-----+-------
12 | toto
(1 row)
select * from audit_portal_user;
uid | uname | who | what | ts
-----+-------+----------+--------+----------------------------
12 | toto | postgres | UPDATE | 2020-06-01 10:20:36.549257
(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

Using variables in a plpgsql function

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();

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