Adding new column while CREATE TABLE event trigger fired - postgresql

The goal :
Adding some columns and constraints while creating some tables when the table name begin by "T_R_..."
The code :
CREATE OR REPLACE FUNCTION complete_table()
RETURNS event_trigger
AS
$$
DECLARE
alter_query TEXT;
r RECORD;
BEGIN
IF tg_tag = 'CREATE TABLE'
THEN
r := pg_event_trigger_ddl_commands();
IF r.object_identity LIKE '%.T?_R?_%' ESCAPE '?'
THEN
alter_query = format('ALTER TABLE %s ADD %s_CODE CHAR(16) CONSTRAINT UK_%S_CODE UNIQUE;',
r.object_identity, RIGHT(r.object_identity, 3), RIGHT(r.object_identity, 3));
EXECUTE alter_query;
END IF;
END IF;
END;
$$ LANGUAGE plpgsql;
For the "procedure"... ANd now the trigger :
CREATE EVENT TRIGGER catch_table
ON ddl_command_end
EXECUTE PROCEDURE complete_table();
And finally the test :
CREATE TABLE public.T_R_ABC (ABC_ID INT PRIMARY KEY);
Now, the trouble :
ERROR: ERREUR: la requête « SELECT pg_event_trigger_ddl_commands() » a renvoyé plus d'une ligne
CONTEXT: fonction PL/pgSQL complete_table(), ligne 8 à affectation
Which in english is something like : The query ... returns more than one row ... line 8 when affecting

The function pg_event_trigger_dll_commands() returns several rows: the table itself and the index. You are getting the error that you're getting because you're attempting to assign several rows to a single record. You need to iterate through them and check to make sure you're looking at object_type = 'table'. Also the check for the start of the table name should be case insensitive (ILIKE vs LIKE) and within the format function UK_%S_CODE needs to be UK_%s_CODE (lowercase s):
CREATE OR REPLACE FUNCTION complete_table()
RETURNS event_trigger
LANGUAGE plpgsql
AS $function$
DECLARE
alter_query TEXT;
r RECORD;
BEGIN
IF tg_tag = 'CREATE TABLE'
THEN
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands()
LOOP
IF r.object_type = 'table' and r.object_identity ILIKE '%.T?_R?_%' ESCAPE '?'
THEN
alter_query = format('ALTER TABLE %s ADD %s_CODE CHAR(16) CONSTRAINT UK_%s_CODE UNIQUE;',
r.object_identity, RIGHT(r.object_identity, 3), RIGHT(r.object_identity, 3));
EXECUTE alter_query;
END IF;
END LOOP;
END IF;
END;
$function$;

Related

postgresql for loop script in text form can not be executed

I am trying to write function in postgresql, that creates temp_table with columns table_name text, table_rec jsonb and fill it through for loop with table names from my table containing names of tables and records in json. I have the for loop in string and I want to execute it. But it doesnt work.
I have variable rec record, sql_query text and tab_name text and I want to do this:
CREATE OR REPLACE FUNCTION public.test51(
)
RETURNS TABLE(tabel_name text, record_json jsonb)
LANGUAGE 'plpgsql'
COST 100
VOLATILE
ROWS 1000
AS $BODY$
declare
rec record;
tabel_name text;
tabel_names text[];
counter integer := 1;
sql_query text;
limit_for_sending integer;
rec_count integer;
begin
select into tabel_names array(select "TABLE_NAME" from public."TABLES");
create temp table temp_tab(tab_nam text, recik jsonb);
while array_length(tabel_names, 1) >= counter loop
tabel_name := '"' || tabel_names[counter] || '"';
select into limit_for_sending "TABLE_LIMIT_FOR_SENDING_DATA" from public."TABLES" where "TABLE_NAME" = tabel_name;
sql_query := 'select count(*) from public.' || tabel_name;
execute sql_query into rec_count;
if (rec_count >= limit_for_sending and limit_for_sending is not null) then
sql_query := 'for rec in select * from public.' || tabel_name || '
loop
insert into temp_tab
select ' || tabel_name || ', to_jsonb(rec);
end loop';
execute sql_query;
end if;
counter := counter + 1;
end loop;
return query
select * from temp_tabik;
drop table temp_tabik;
end;
$BODY$;
Thank you for response.
It seems you have some table that contains the information for which tables you want to return all rows as JSONB. And that meta-table also contains a column that sets a threshold under which the rows should not be returned.
You don't need the temp table or an array to store the table names. You can iterate through the query on the TABLES table and run the dynamic SQL directly in that loop.
return query in PL/pgSQL doesn't terminate the function, it just appends the result of the query to the result of the function.
Dynamic SQL is best created using the format() function because it is easier to read and using the %I placeholder will properly deal with quoted identifiers (which is really important as you are using those dreaded upper case table names)
As far as I can tell, your function can be simplified to:
CREATE OR REPLACE FUNCTION public.test51()
RETURNS TABLE(tabel_name text, record_json jsonb)
LANGUAGE plpgsql
AS
$BODY$
declare
rec record;
sql_query text;
rec_count bigint;
begin
for rec in
select "TABLE_NAME" as table_name, "TABLE_LIMIT_FOR_SENDING_DATA" as rec_limit
from public."TABLES"
loop
if rec.rec_limit is not null then
execute format('select count(*) from %I', rec.table_name)
into rec_count;
end if;
if (rec.rec_limit is not null and rec_count >= rec.rec_limit) then
sql_query := format('select %L, to_jsonb(t) from %I as t', rec.table_name, rec.table_name);
return query execute sql_query;
end if;
end loop;
end;
$BODY$;
Some notes
the language name is an identifier and should not be enclosed in single quotes. This syntax is deprecated and might be removed in a future version so don't get used to it.
you should really avoid those dreaded quoted identifiers. They are much more trouble than they are worth it. See the Postgres wiki for details.

PG DDL event trigger does not work properly

I am trying to intercept a CREATE TABLE by an event trigger in PostgreSQL, to forbid the creation of table that does not comply to some naming rules. My code is as follow:
CREATE OR REPLACE FUNCTION e_ddl_create_table_func()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
obj record;
BEGIN
FOR obj IN SELECT *
FROM pg_event_trigger_ddl_commands()
WHERE command_tag in ('CREATE TABLE')
LOOP
if NOT obj.object_identity LIKE 't?_%' ESCAPE '?'
THEN
raise EXCEPTION 'The table name must begin with t_';
end if;
END LOOP;
END;
$$;
CREATE EVENT TRIGGER trg_create_table ON ddl_command_end
WHEN TAG IN ('CREATE TABLE')
EXECUTE PROCEDURE e_ddl_create_table_func();
When I try with:
CREATE TABLE t_toto3 (i INT)
I have systematically the following error:
ERROR: The table name must begin with t_
CONTEXT: fonction PL/pgSQL e_ddl_create_table_func(), ligne 11 à RAISE
What am I missing ?
Per the docs, object_identity is schema-qualified. It will be coming in as 'public.t_toto3' in your example (unless you have a very nonstandard setup with some other default schema); you can get only the table component by passing it through parse_ident() and extracting the 2nd item. (Note the extra parens around the final parse_ident() so that the array lookup is parsed correctly.)
testdb=# select 'public.t_toto3' LIKE 't?_%' ESCAPE '?';
?column?
----------
f
(1 row)
testdb=# select parse_ident('public.t_toto3');
parse_ident
------------------
{public,t_toto3}
(1 row)
testdb=# select (parse_ident('public.t_toto3'))[2] LIKE 't?_%' ESCAPE '?';
?column?
----------
t
(1 row)
yes it works with parse_ident. The solution is :
CREATE OR REPLACE FUNCTION e_ddl_create_table_func()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
obj record;
BEGIN
FOR obj IN SELECT *
FROM pg_event_trigger_ddl_commands()
WHERE command_tag in ('CREATE TABLE')
LOOP
if NOT (parse_ident(obj.object_identity))[2] LIKE 't?_%' ESCAPE '?'
THEN
raise EXCEPTION 'The table name must begin with t_';
end if;
END LOOP;
END;
$$;
CREATE EVENT TRIGGER trg_create_table ON ddl_command_end
WHEN TAG IN ('CREATE TABLE')
EXECUTE PROCEDURE e_ddl_create_table_func();

How To Restrict Delete using PL/pgSQL trigger?

If the client user is trying to delete more than 5 records from a Table i want to restrict that using a trigger. I have a basic idea to do that but i don't know how to implement the Idea. I appreciate any HELP.
Basic Idea : In Trigger IF TG_OP = Delete and the count of records to be deleted are more than 5 then Restrict.
CREATE TRIGGER adjust_count_trigger BEFORE DELETE ON schemaname.tablename
FOR EACH ROW EXECUTE PROCEDURE public.adjust_count();
CREATE OR REPLACE FUNCTION adjust_count()
RETURNS TRIGGER AS
$$
DECLARE
num_rows int;
num_rows1 int;
BEGIN
IF TG_OP = 'DELETE' THEN
EXECUTE 'select count(*) from '||TG_TABLE_SCHEMA ||'.'||TG_RELNAME ||' where oid = old.oid ' into num_rows ;
IF num_rows > 5 Then
RAISE NOTICE 'Cannot Delete More than 5 Records , % ', num_rows ;
END IF ;
END IF ;
RETURN OLD;
END;
$$
LANGUAGE 'plpgsql';
In earlier versions of Postgres you can simulate a transition table introduced in Postgres 10. You need two triggers.
create trigger before_delete
before delete on my_table
for each row execute procedure before_delete();
create trigger after_delete
after delete on my_table
for each statement execute procedure after_delete();
In the first trigger create a temp table and insert a row into it:
create or replace function before_delete()
returns trigger language plpgsql as $$
begin
create temp table if not exists deleted_rows_of_my_table (dummy int);
insert into deleted_rows_of_my_table values (1);
return old;
end $$;
In the other trigger count rows of the temp table and drop it:
create or replace function after_delete()
returns trigger language plpgsql as $$
declare
num_rows bigint;
begin
select count(*) from deleted_rows_of_my_table into num_rows;
drop table deleted_rows_of_my_table;
if num_rows > 5 then
raise exception 'Cannot Delete More than 5 Records , % ', num_rows;
end if;
return null;
end $$;
The above solution may seem a bit hacky but it is safe if only the temp table does not exist before delete (do not use the same name of the temp table for multiple tables).
Test it in rextester.
You can easily do that with the new transition relation feature from PostgreSQL v10:
CREATE OR REPLACE FUNCTION forbid_more_than() RETURNS trigger
LANGUAGE plpgsql AS
$$DECLARE
n bigint := TG_ARGV[0];
BEGIN
IF (SELECT count(*) FROM deleted_rows) <= n IS NOT TRUE
THEN
RAISE EXCEPTION 'More than % rows deleted', n;
END IF;
RETURN OLD;
END;$$;
CREATE TRIGGER forbid_more_than_5
AFTER DELETE ON mytable
REFERENCING OLD TABLE AS deleted_rows
FOR EACH STATEMENT
EXECUTE PROCEDURE forbid_more_than(5);

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;

For loop with dynamic table name in Postgresql 9.1?

I have a plpgslq function which does some data processing and would like to write a for loop, however my table name is not known at design time. Is there any possible way to achieve this? Here is sample code snippet of what I want to achieve:
-- Function: check_data()
-- DROP FUNCTION check_data();
CREATE OR REPLACE FUNCTION check_data()
RETURNS character varying AS
$BODY$declare
dyn_rec record;
tbl_name record;
begin
-- sample dynamic tables
tbl_name := 'cars';
tbl_name := 'trucks';
tbl_name := 'bicycles';
for dyn_rec in select * from format($$s%$$,tbl_name) loop
raise notice 'item is %',dyn_rec.item_no;
end loop;
return 'Processing Ok';
end;$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION check_data()
OWNER TO postgres;
You cannot use a variable as table or column identifier in plpgsql embedded SQL ever. A solution is dynamic SQL - EXECUTE or FOR IN EXECUTE statements:
DO $$
DECLARE
tables text[] = ARRAY['table1','table2'];
table_name text;
rec record;
BEGIN
FOREACH table_name IN ARRAY tables
LOOP
FOR r IN EXECUTE format('SELECT * FROM %I', table_name)
LOOP
RAISE NOTICE '%', rec;
END LOOP;
END LOOP;
END; $$