How to create a trigger function dynamically in pgsql? - postgresql

I want to write a pgsql function to create trigger dynamically. For example, a trigger to count insertions in each table. I've tried EXECUTE like this:
CREATE FUNCTION trigen(tbl text) RETURNS void AS $$
BEGIN
EXECUTE format(
'CREATE FUNCTION %s_insertCnt() RETURNS TRIGGER AS $$
BEGIN
UPDATE insertions SET n = n + 1 WHERE tablename = %s;
END
$$ LANGUAGE plpgsql', tbl, quote_nullable(tbl));
EXECUTE format('CREATE TRIGGER %s_inCnt BEFORE INSERT ON %s
FOR EACH ROW EXECUTE PROCEDURE %s_insertCnt();', tbl, tbl, tbl);
END
$$ LANGUAGE plpgsql
But this approach doesn't work. A lot of syntax error occurred when I import this code. It seems that EXECUTE cannot execute a function creation.
What else can I do to create trigger functions dynamically?

The two $$ sections were getting confused. By using the $name$ syntax instead you can separate these.
Also the trigger was missing a RETURN.
CREATE OR REPLACE FUNCTION trigen(tbl text) RETURNS void AS $T1$
BEGIN
EXECUTE format(
'CREATE FUNCTION %s_insertCnt() RETURNS TRIGGER AS $T2$
BEGIN
UPDATE insertions SET n = n + 1 WHERE tablename = %s;
RETURN NEW;
END
$T2$ LANGUAGE plpgsql', tbl, quote_nullable(tbl));
EXECUTE format('CREATE TRIGGER %s_inCnt BEFORE INSERT ON %s
FOR EACH ROW EXECUTE PROCEDURE %s_insertCnt();', tbl, tbl, tbl);
END
$T1$ 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;

Save execute results into a table

Below is a simplified postgres stored procedure I am trying to run:
create or replace procedure my_schema.tst(suffix varchar)
as $$
begin
execute(' select *
into my_schema.MyTable_'||suffix||'
From my_schema.MyTable
');
end;
$$
language plpgsql;
When I attempt to run using something like:
call my_schema.tst('test');
I get this error Invalid operation: EXECUTE of SELECT ... INTO is not supported;
Is it possible to execute a dynamic query that creates a new table? I have seen examples that look like:
Execute('... some query ...') into Table;
but for my use case I need the resulting tablename to be passed as a variable.
In PostgreSQL you can use INSERT INTO tname SELECT...
create or replace procedure my_schema.tst(suffix varchar)
as $$
begin
execute ' INSERT INTO my_schema.MyTable_'||suffix||' SELECT *
FROM my_schema.MyTable
';
end;
$$
language plpgsql;
or Use CREATE TABLE tname AS SELECT..., :
create or replace procedure my_schema.tst(suffix varchar)
as $$
begin
execute ' CREATE TABLE my_schema.MyTable_'||suffix||' as SELECT *
FROM my_schema.MyTable
';
end;
$$
language plpgsql;

How to pass NEW.* to EXECUTE in trigger function

I have a simple mission is inserting huge MD5 values into tables (partitioned table), and have created a trigger and also a trigger function to instead of INSERT operation. And in function I checked the first two characters of NEW.md5 to determine which table should be inserted.
DECLARE
tb text;
BEGIN
IF TG_OP = 'INSERT' THEN
tb = 'samples_' || left(NEW.md5, 2);
EXECUTE(format('INSERT INTO %s VALUES (%s);', tb, NEW.*)); <- WRONG
END IF;
RETURN NULL;
END;
The question is how to concat the NEW.* into the SQL statement?
Best with the USING clause of EXECUTE:
CREATE FUNCTION foo ()
RETURNS trigger AS
$func$
BEGIN
IF TG_OP = 'INSERT' THEN
EXECUTE format('INSERT INTO %s SELECT $1.*'
, 'samples_' || left(NEW.md5, 2);
USING NEW;
END IF;
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
And EXECUTE does not require parentheses.
And you are aware that identifiers are folded to lower case unless quoted where necessary (%I instead of %s in format()).
More details:
INSERT with dynamic table name in trigger function
How to dynamically use TG_TABLE_NAME in PostgreSQL 8.2?

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;

How to log delete queries on Postgresql?

I created a function which writes information about table deletions.
And another function which simply adds a trigger call after delete.
But I would like to store the whole row as string into my table.
According to Postgresql Documentation it should work by adding "OLD.*" into a text based column. But it fails telling me that I try to put too many columns into this table.
OLD is from type RECORD. And i want to have it in my text field like "value1,value2,value3" or it could be "colname:value,colname2:value". I dont care, I just want to see the row which has been deleted.
Another approach can be to log all delete queries from pg_stat_activity. But I don't know how to do that. Simply accessing pg_stat_activity every second would cause too much traffic I guess.
My table is simple:
create table delete_history (date timestamp, tablename varchar(100), data text);
This is my function:
CREATE or REPLACE FUNCTION ondelete() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO delete_history VALUES (CURRENT_TIMESTAMP, TG_TABLE_NAME, OLD.*);
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
This is my trigger:
CREATE OR REPLACE FUNCTION history_create_triggers() RETURNS void
AS $$
DECLARE
r RECORD;
BEGIN
FOR r IN SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type='BASE TABLE' LOOP
EXECUTE 'CREATE TRIGGER log_history AFTER DELETE ON public.' || r.table_name || ' FOR EACH ROW EXECUTE PROCEDURE ondelete();';
END LOOP;
END;
$$ LANGUAGE plpgsql;
You can convert type record into text:
CREATE or REPLACE FUNCTION ondelete() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO delete_history VALUES (CURRENT_TIMESTAMP, TG_TABLE_NAME, OLD::text);
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
sql fiddle demo
another approach could be converting your row into JSON with row_to_json function (if you have version 9.2):
CREATE or REPLACE FUNCTION ondelete() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO delete_history VALUES (CURRENT_TIMESTAMP, TG_TABLE_NAME, row_to_json(OLD));
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
sql fiddle demo
Another approach can be convert your data to hstore
CREATE or REPLACE FUNCTION ondelete() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO delete_history VALUES (CURRENT_TIMESTAMP, TG_TABLE_NAME, hstore(OLD));
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
I can't test it now - sqlfiddle is not allowing to use hstore.