PG DDL event trigger does not work properly - postgresql

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

Related

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;

pg_event_trigger_ddl_commands() with DROP TABLE command

CREATE OR REPLACE FUNCTION ddl_command_test()
RETURNS event_trigger
AS $$
DECLARE
obj record;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands()
LOOP
RAISE NOTICE '% commnad on oid: %',
tg_tag,
obj.objid;
RAISE NOTICE 'triggered';
END LOOP;
END; $$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER test_ddl ON ddl_command_end
EXECUTE FUNCTION ddl_command_test();
While pg_event_trigger_ddl_commands() function returns info for table creation:
CREATE TABLE test_table(
col int
);
It not prints notification message when table is dropped:
DROP TABLE test_table;
Don't get why, because event-trigger-matrix shows that ddl_​command_​end includes DROP TABLE command also.
Although the documentation event-trigger-matrix says that ddl_command_end can be used for DROP statements, I also struggled with that issue in the past.
Thereby, I found this workaround, which involves creating a specific function that fetches FROM pg_event_trigger_dropped_objects(), to notify when the DROP statement is used.
CREATE OR REPLACE FUNCTION ddl_drop_command_test()
RETURNS event_trigger
AS $$
DECLARE
obj record;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
LOOP
RAISE NOTICE '% commnad on oid: %',
tg_tag,
obj.objid;
RAISE NOTICE 'triggered';
END LOOP;
END; $$ LANGUAGE plpgsql;
Further, you need to use ON sql_drop to create your event trigger. The WHEN TAG can be incremented with DROP SCHEMA and other Postgres objects.
CREATE EVENT TRIGGER drop_test ON sql_drop
WHEN TAG IN ('DROP TABLE')
EXECUTE PROCEDURE ddl_drop_command_test();

how to create event trigger for create table or select into

i want create event trigger for create table or select into,
eg:
when create table xxxx must table name bigen with 'temp'
my code
CREATE OR REPLACE FUNCTION create_table_func()
RETURNS event_trigger
AS
$$
DECLARE
V_TABLE name := TG_TABLE_NAME;
BEGIN
if V_TABLE !~ '^temp'
then
RAISE EXCEPTION 'must bigen with temp';
end if;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE EVENT TRIGGER create_table_1 ON ddl_command_start
WHEN TAG IN ('SELECT INTO')
EXECUTE PROCEDURE create_table_func();
but when execute
select * into test11 from test_bak
[Err] ERROR: column "tg_table_name" does not exist
this is my code ,it's meet my needs
code:
CREATE OR REPLACE FUNCTION trg_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 ('SELECT INTO','CREATE TABLE','CREATE TABLE AS')
LOOP
if obj.object_identity !~ 'public.temp_'
THEN
raise EXCEPTION 'The table name must begin with temp_';
end if;
END LOOP;
END;
$$;
CREATE EVENT TRIGGER trg_create_table ON ddl_command_end
WHEN TAG IN ('SELECT INTO','CREATE TABLE','CREATE TABLE AS')
EXECUTE PROCEDURE trg_create_table_func();
out recods
[Err] ERROR: The table name must begin with temp_
CONTEXT: PL/pgSQL function trg_create_table_func() line 10 at RAISE
it's cool ~
The special variable TG_TABLE_NAME is only supported in normal triggers, not in event triggers (there is not always an associated table!).
The documentation has a list of functions that can return context information in an event trigger.
You could use pg_event_trigger_ddl_commands() to get the information you need, but that only works in ddl_command_end event triggers. That should work for you; I don't see a reason why the trigger should not run at the end of the statement.

not start trigger on view from pg_stat_activity

In postgres not real create trigger on pg_stat_activity, becouse i create my view based on pg_stat_activity and create trigger.
DROP FUNCTION IF EXISTS get_sa() CASCADE;
DROP FUNCTION IF EXISTS f_call_count_conn();
DROP FUNCTION IF EXISTS f_update_count_conn();
CREATE OR REPLACE FUNCTION get_sa() RETURNS SETOF pg_stat_activity AS
$$ SELECT * FROM pg_catalog.pg_stat_activity; $$
LANGUAGE sql
VOLATILE
SECURITY DEFINER;
CREATE OR REPLACE VIEW pg_stat_activity_allusers AS SELECT * FROM get_sa();
GRANT SELECT ON pg_stat_activity_allusers TO public;
CREATE OR REPLACE FUNCTION f_call_count_conn()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF TG_OP = 'INSERT' THEN
COPY (SELECT time_change, count FROM count_conn) TO '/tmp/query.csv' (format csv, delimiter ';');
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
COPY (SELECT time_change, count FROM count_conn) TO '/tmp/query.csv' (format csv, delimiter ';');
RETURN OLD;
END IF;
-- PERFORM f_update_count_conn();
-- RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER t_check_activity_conn
INSTEAD OF INSERT OR DELETE ON pg_stat_activity_allusers
FOR EACH ROW
EXECUTE PROCEDURE f_call_count_conn();
CREATE FUNCTION f_update_count_conn()
RETURNS VOID
AS
$BODY$
BEGIN
insert into count_conn (time_change, count)
values (NOW(), (select count(*)
from pg_stat_activity_allusers));
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
This code is not working, but when i replace my view (pg_stat_activity_allusers) on real table and change this table, my trigger work. Why? Thank you!
Your code worked for me on Postgres 9.5, except I manually called f_update_count_conn() to populate count_conn, because you're not (or no longer) calling it anywhere.
# select f_update_count_conn();
f_update_count_conn
---------------------
(1 row)
mw=# select * from count_conn;
time_change | count
----------------------------+-------
2017-02-03 17:22:34.846179 | 1
(1 row)
mw=# insert into pg_stat_activity_allusers(datid) values(123456::oid);
INSERT 0 1
mw=#
[1]+ Stopped '/Applications/Postgres.app/Contents/Versions/9.5/bin'/psql -p5432
$ cat /tmp/query.csv
2017-02-03 17:22:34.846179;1

Variables for identifiers inside IF EXISTS in a plpgsql function

CREATE OR REPLACE FUNCTION drop_now()
RETURNS void AS
$BODY$
DECLARE
row record;
BEGIN
RAISE INFO 'in';
FOR row IN
select relname from pg_stat_user_tables
WHERE schemaname='public' AND relname LIKE '%test%'
LOOP
IF EXISTS(SELECT row.relname.tm FROM row.relname
WHERE row.relname.tm < current_timestamp - INTERVAL '90 minutes'
LIMIT 1)
THEN
-- EXECUTE 'DROP TABLE ' || quote_ident(row.relname);
RAISE INFO 'Dropped table: %', quote_ident(row.relname);
END IF;
END LOOP;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Could you tell me how to use variables in SELECT which is inside IF EXISTS? At the present moment, row.relname.tm and row.relname are treated literally which is not I want.
CREATE OR REPLACE FUNCTION drop_now()
RETURNS void AS
$func$
DECLARE
_tbl regclass;
_found int;
BEGIN
FOR _tbl IN
SELECT relid
FROM pg_stat_user_tables
WHERE schemaname = 'public'
AND relname LIKE '%test%'
LOOP
EXECUTE format($f$SELECT 1 FROM %s
WHERE tm < now() - interval '90 min'$f$, _tbl);
GET DIAGNOSTICS _found = ROW_COUNT;
IF _found > 0 THEN
-- EXECUTE 'DROP TABLE ' || _tbl;
RAISE NOTICE 'Dropped table: %', _tbl;
END IF;
END LOOP;
END
$func$ LANGUAGE plpgsql;
Major points
row is a reserved word in the SQL standard. It's use is allowed in Postgres, but it's still unwise. I make it a habbit to prepend psql variable with an underscore _ to avoid any naming conflicts.
You don't don't select the whole row anyway, just the table name in this example. Best use a variable of type regclass, thereby avoiding SQL injection by way of illegal table names automatically. Details in this related answer:
Table name as a PostgreSQL function parameter
You don't need LIMIT in an EXISTS expression, which only checks for the existence of any rows. And you don't need meaningful target columns for the same reason. Just write SELECT 1 or SELECT * or something.
You need dynamic SQL for queries with variable identifiers. Plain SQL does not allow for that. I.e.: build a query string and EXECUTE it. Details in this closely related answer:
Dynamic SQL (EXECUTE) as condition for IF statement
The same is true for a DROP statement, should you want to run it. I added a comment.
You'll need to build your query as a string then execute that - see the section on executing dynamic commands in the plpgsql section of the manual.