unable to create dynamic table using function with execute format option in postgresql - plpgsql

i am trying to create dynamic table using function however i am getting error.
my code as below.
create or replace function fn_while_loop_upd_table(val int) returns BOOLEAN as
$$
DECLARE lv_count int = 0;
lv_in int = 100;
mybool BOOLEAN = false;
t1 varchar(30) = val;
BEGIN
execute format('drop table if exists t_tab%%t1');
EXECUTE format('create table if not exists t_tab %%t1(myval int)');
while lv_count <= VAL
loop
lv_in = lv_in + 1;
insert into t_tab values(lv_in);
lv_count = lv_count + 1;
end loop;
RETURN mybool;
end;
$$
LANGUAGE plpgsql;
note i have tried with single % also error as below.
ERROR: syntax error at or near "%"
LINE 1: create table if not exists t_tab %t1(myval int)
^
QUERY: create table if not exists t_tab %t1(myval int)
CONTEXT: PL/pgSQL function fn_while_loop_upd_table(integer) line 8 at EXECUTE
SQL state: 42601
create or replace function fn_while_loop_upd_table(val int) returns BOOLEAN as
$$
DECLARE lv_count int = 0;
lv_in int = 100;
mybool BOOLEAN = false;
t1 varchar(30) = val;
BEGIN
execute format('drop table if exists t_tab%%t1');
EXECUTE format('create table if not exists t_tab %%t1(myval int)');
while lv_count <= VAL
loop
lv_in = lv_in + 1;
insert into t_tab values(lv_in);
lv_count = lv_count + 1;
end loop;
RETURN mybool;
end;
$$
LANGUAGE plpgsql;
error
ERROR: syntax error at or near "%"
LINE 1: create table if not exists t_tab %t1(myval int)
^
QUERY: create table if not exists t_tab %t1(myval int)
CONTEXT: PL/pgSQL function fn_while_loop_upd_table(integer) line 8 at EXECUTE
SQL state: 42601

SQL and PLpgSQL doesn't do implicit variables replacement in strings. The syntax %%var is just wrong. The behaviour is similar to C or C++.
so
execute format('drop table if exists t_tab%%t1');
EXECUTE format('create table if not exists t_tab %%t1(myval int)');
has nothing common with reality. It should be
execute format('drop table if exists %I', 't_tab' || t1);
EXECUTE format('create table if not exists %I(myval int)', 't_tab' || t1);
t1 should be declared as int type (not varchar).
PLpgSQL knows FOR cycle, what should be used, when number of iterations is known before:
CREATE OR REPLACE FUNCTION fn(n int)
RETURNS void AS $$
BEGIN
FOR i IN 1..n
LOOP
EXECUTE format('drop table if exists %I', 't_tab' || i);
EXECUTE format('create table if not exists %I(myval int)', 't_tab' || i);
END LOOP;
END;
$$ LANGUAGE plpgsql;

Related

Argument not taking the value from Postgres function

I have a simple Postgres function where I want to take table_name as a parameter and pass it into an argument and delete the data from table by condition.
CREATE OR REPLACE FUNCTION cdc.audit_refresh(tablename text)
RETURNS integer AS
$$
BEGIN
delete from tablename where id<4;
RETURN(select 1);
END;
$$ LANGUAGE plpgsql;
select cdc.audit_refresh('cdc.adf_test');
But it throws out an error that tablename
ERROR: relation "tablename" does not exist in the delete statement.(refer snapshot)
What you want to achieve is to execute Dynamic SQL statements. You can do this with EXECUTE. See more here
CREATE OR REPLACE FUNCTION audit_refresh(tablename text)
RETURNS integer AS
$$
DECLARE
stmt TEXT;
BEGIN
stmt = 'delete from '||tablename||' where id<4;';
EXECUTE stmt;
RETURN 1;
END
$$ LANGUAGE plpgsql;

Cursor not found

i have created procedure, inside used cursor to update the some data, while calling the procedure it's getting the error.
create or REPLACE PROCEDURE bal_upd(p_id int) as
$$
DECLARE rc record;
----- cursor
bal_upd1 CURSOR (p_id int)
for
select * from tbal where custid = p_id;
begin
open bal_upd1 (p_id);
loop
FETCH bal_upd1 into rc;
exit when not found;
update t_trans set balance = balance + rc.trans;
COMMIT;
end loop;
close bal_upd1;
end;
$$ LANGUAGE plpgsql;
call bal_upd(1)
ERROR: cursor "bal_upd1" does not exist
CONTEXT: PL/pgSQL function bal_upd(integer) line 12 at FETCH
SQL state: 34000
create or REPLACE PROCEDURE bal_upd(p_id int) as
$$
DECLARE rc record;
----- cursor
bal_upd1 CURSOR (p_id int)
for
select * from tbal where custid = p_id;
begin
open bal_upd1 (p_id);
loop
FETCH bal_upd1 into rc;
exit when not found;
update t_trans set balance = balance + rc.trans;
COMMIT;
end loop;
close bal_upd1;
end;
$$ LANGUAGE plpgsql;
call bal_upd(1)
ERROR: cursor "bal_upd1" does not exist
CONTEXT: PL/pgSQL function bal_upd(integer) line 12 at FETCH
SQL state: 34000
You don't need a function or a loop for that:
UPDATE t_trans
SET balance = t_trans.balance + t.trans
FROM (SELECT sum(trans) AS trans
FROM tbal
GROUP BY custid) AS t
WHERE t_trans.custid = t.custid;
I tried, failed. I just found just use for loop (implicit cursor) is far more simple.
BEGIN;
CREATE temp TABLE tbal (
custid bigint
, trans numeric
);
INSERT INTO tbal VALUES (1 , 1);
INSERT INTO tbal VALUES (1 , 2);
CREATE temp TABLE t_trans (
custid bigint
, balance numeric
);
INSERT INTO t_trans VALUES (1 , 10);
COMMIT;
CREATE OR REPLACE PROCEDURE bal_upd (bigint)
AS $func$
DECLARE
rc record;
BEGIN
FOR rc IN
SELECT
*
FROM
tbal
WHERE
custid = $1 LOOP
RAISE NOTICE 'custid: %, trans: % ' , rc.custid , rc.trans;
UPDATE
t_trans ta
SET
balance = balance + (rc.trans)
WHERE
ta.custid = (rc.custid);
END LOOP;
END;
$func$
LANGUAGE plpgsql;
Then call it. CALL bal_upd(1);

Save dynamic query to variable in postgres stored procedure

I have the following postgres stored procedure:
CREATE OR REPLACE PROCEDURE
schema.MyProcedure()
AS $$
DECLARE
RowCount int;
BEGIN
SELECT cnt INTO RowCount
FROM (
SELECT COUNT(*) AS cnt
FROM MySchema.MyTable
) AS sub;
RAISE NOTICE 'RowCount: %', RowCount;
END;
$$
LANGUAGE plpgsql;
which "prints" out the row count of the static table MySchema.MyTable. How can it make it so I pass the Table and Schema name as an input.
eg:
CREATE OR REPLACE PROCEDURE
schema.MyProcedure(MySchema_In varchar, MyTable_In varchar)
AS $$
DECLARE
RowCount int;
BEGIN
SELECT cnt INTO RowCount
FROM (
SELECT COUNT(*) AS cnt
FROM || **MySchema_In** || . || **MyTable_In** ||
) AS sub;
RAISE NOTICE 'RowCount: %', RowCount;
END;
$$
LANGUAGE plpgsql;
You should use format() instead of concatenating the strings with || and then EXECUTE ... INTO to get the query's result, e.g.
CREATE OR REPLACE PROCEDURE MyProcedure(MySchema_In varchar, MyTable_In varchar)
AS $$
DECLARE RowCount int;
BEGIN
EXECUTE FORMAT('SELECT count(*) FROM %I.%I',$1,$2) INTO RowCount;
RAISE NOTICE 'RowCount: %', RowCount;
END;
$$
LANGUAGE plpgsql;

PostgreSQL update trigger Comparing Hstore values

I am creating trigger in PostgresSQL. On update I would like to compare all of the values in a Hstore column and update changes in my mirror table. I managed to get names of my columns in variable k but I am not able to get values using it from NEW and OLD.
CREATE OR REPLACE FUNCTION function_replication() RETURNS TRIGGER AS
$BODY$
DECLARE
k text;
BEGIN
FOR k IN SELECT key FROM EACH(hstore(NEW)) LOOP
IF NEW.k != OLD.k THEN
EXECUTE 'UPDATE ' || TG_TABLE_NAME || '_2' || 'SET ' || k || '=' || new.k || ' WHERE ID=$1.ID;' USING OLD;
END IF;
END LOOP;
RETURN NEW;
END;
$BODY$
language plpgsql;
You should operate on hstore representations of the records new and old. Also, use the format() function for better control and readibility.
create or replace function function_replication()
returns trigger as
$body$
declare
newh hstore = hstore(new);
oldh hstore = hstore(old);
key text;
begin
foreach key in array akeys(newh) loop
if newh->key != oldh->key then
execute format(
'update %s_2 set %s = %L where id = %s',
tg_table_name, key, newh->key, oldh->'id');
end if;
end loop;
return new;
end;
$body$
language plpgsql;
Another version - with minimalistic numbers of updates - in partially functional design (where it is possible).
This trigger should be AFTER trigger, to be ensured correct behave.
CREATE OR REPLACE FUNCTION function_replication()
RETURNS trigger AS $$
DECLARE
newh hstore;
oldh hstore;
update_vec text[];
pair text[];
BEGIN
IF new IS DISTINCT FROM old THEN
IF new.id <> old.id THEN
RAISE EXCEPTION 'id should be immutable';
END IF;
newh := hstore(new); oldh := hstore(old); update_vec := '{}';
FOREACH pair SLICE 1 IN ARRAY hstore_to_matrix(newh - oldh)
LOOP
update_vec := update_vec || format('%I = %L', pair[1], pair[2]);
END LOOP;
EXECUTE
format('UPDATE %I SET %s WHERE id = $1',
tg_table_name || '_2',
array_to_string(update_vec, ', '))
USING old.id;
END IF;
RETURN NEW; -- the value is not important in AFTER trg
END;
$$ LANGUAGE plpgsql;
CREATE TABLE foo(id int PRIMARY KEY, a int, b int);
CREATE TABLE foo_2(LIKE foo INCLUDING ALL);
CREATE TRIGGER xxx AFTER UPDATE ON foo
FOR EACH ROW EXECUTE PROCEDURE function_replication();
INSERT INTO foo VALUES(1, NULL, NULL);
INSERT INTO foo VALUES(2, 1,1);
INSERT INTO foo_2 VALUES(1, NULL, NULL);
INSERT INTO foo_2 VALUES(2, 1,1);
UPDATE foo SET a = 20, b = 30 WHERE id = 1;
UPDATE foo SET a = NULL WHERE id = 1;
This code is little bit more complex, but all what should be escaped is escaped and reduce number of executed UPDATE commands. UPDATE is full SQL command and the overhead of full SQL commands should be significantly higher than code that reduce number of full SQL commands.

How to keep looping even error happend?

I wrote a PL/pgsql to batch create index on tables
CREATE OR REPLACE FUNCTION create_index() RETURNS void AS
$BODY$
DECLARE
r INTEGER;
BEGIN
FOR r IN 1..1000
LOOP
EXECUTE format(
' CREATE INDEX idx_abc_id_' || r::text ||
' ON abc_id_' || r::text ||
' USING btree
(key);');
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;
it has one problem, if partition abc_500 doesn't exist, then the how create index function will fail and do nothing.
How to make loop keep going through even if create_index made an error on one of the table in between?
I think a better approach would be to not hardcode the number for the loop, but iterate over the existing tables:
CREATE OR REPLACE FUNCTION create_index() RETURNS void AS
$BODY$
DECLARE
r record;
BEGIN
FOR r IN select tablename, regexp_replace(tablename, '[^0-9]+','') as idx_nr
from pg_tables
where tablename ~ 'abc_id_[0-9]+'
LOOP
EXECUTE format('CREATE INDEX %I ON %I USING btree (key)',
'idx_abc_id_'||r.idx_nr,
r.tablename);
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;
When you use the format() function is better to use the proper place holders for identifiers.
If you also want to ignore any error when creating the index on an existing table, you need to catch the exception and ignore it:
CREATE OR REPLACE FUNCTION create_index() RETURNS void AS
$BODY$
DECLARE
r record;
msg text;
BEGIN
FOR r IN select tablename, regexp_replace(tablename, '[^0-9]+','') as idx_nr
from pg_tables
where tablename ~ 'abc_id_[0-9]+'
LOOP
BEGIN
EXECUTE format('CREATE INDEX %I ON %I USING btree (key)',
'idx_abc_id_'||r.idx_nr,
r.tablename);
EXCEPTION
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS msg = MESSAGE_TEXT;
RAISE NOTICE 'Could not create index for: %, %', r.idx_nr, msg;
END;
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;