Update row in trigger without recursion - postgresql

I have two tables:
CREATE TABLE first (
id text primary key,
updated_at timestamp,
data text
);
CREATE TABLE second (
id text REFERENCES first (id),
book_error text,
);
and I need to update updated_at field in first table always, when any of these tables has updated. I wrote this:
CREATE FUNCTION update_timestamp() RETURNS trigger AS $$
BEGIN
UPDATE first
SET updated_at = current_timestamp
WHERE id = NEW.id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
t text;
BEGIN
FOR t IN
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
LOOP
EXECUTE format('CREATE TRIGGER update_timestamp
BEFORE INSERT OR UPDATE ON %I
FOR EACH ROW EXECUTE PROCEDURE update_timestamp()',
t);
END LOOP;
END;
$$ LANGUAGE plpgsql;
But it's not working because update statement inside my trigger causes call of this trigger again before executing.
How can I do update inside trigger without firing it trigger again?

Per the documentation:
TG_TABLE_NAME
Data type name; the name of the table that caused the trigger invocation.
Use the variable in the trigger function:
CREATE OR REPLACE FUNCTION update_timestamp() RETURNS trigger AS $$
BEGIN
IF TG_TABLE_NAME = 'first' THEN
NEW.updated_at = current_timestamp;
ELSE
UPDATE first
SET updated_at = current_timestamp
WHERE id = NEW.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

From Postgresql documentation,
pg_trigger_depth() - current nesting level of PostgreSQL triggers (0
if not called, directly or indirectly, from inside a trigger)
You can use this inside your trigger function to check if it is called from inside trigger
DO $$
DECLARE
t text;
BEGIN
FOR t IN
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
LOOP
EXECUTE format('CREATE TRIGGER update_timestamp
BEFORE INSERT OR UPDATE ON %I
FOR EACH ROW
WHEN (pg_trigger_depth() = 0)
EXECUTE PROCEDURE update_timestamp()',
t);
END LOOP;
END;
$$ LANGUAGE plpgsql;
test=# Insert into first select 1,now(), 'test';
INSERT 0 1
test=# select * from first;
id | updated_at | data
----+----------------------------+------
1 | 2018-10-29 20:18:25.227281 | test
(1 row)
test=# Insert into second select 1, 'test_error';
INSERT 0 1
test=# select * from first;
id | updated_at | data
----+----------------------------+------
1 | 2018-10-29 20:19:07.456737 | test

Related

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)

Triggers in Postgres: Access NEW fields by name at runtime

In Postgres, someone knows how to substitute the value of the variable in a NEW.variable in a trigger?
For instance, I have a variable with value order_code. I want to execute NEW.variable so that it's getting in fact NEW.order_code.
In detailed:
I have a function to obtain the primary key column of a table:
CREATE FUNCTION getPrimaryKey(_table_name VARCHAR(50))
RETURNS SETOF VARCHAR(50) AS $$
DECLARE
primary_key VARCHAR(50);
BEGIN
FOR primary_key IN SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = _table_name::regclass
AND i.indisprimary LOOP
RETURN NEXT primary_key;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Then I have a trigger to collect some info when an INSERT is done in a table. The procedure in the trigger is called from several triggers from different tables. That's why it's so generic and I have this need.
What I want is to obtain the primary key of the object inserted.
CREATE FUNCTION logAudit()
RETURNS trigger AS $$
DECLARE primary_key VARCHAR(50);
BEGIN
primary_key := getprimarykey(TG_TABLE_NAME::VARCHAR(50));
INSERT INTO test VALUES (TG_TABLE_NAME);
INSERT INTO test VALUES (NEW.primary_key);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_in_client
AFTER INSERT ON tb_client
FOR EACH STATEMENT EXECUTE PROCEDURE logAudit();
The NEW.primary_key is what is causing me issues. I expect primary_key to be the column name of the source table where the insert happened. What I want in NEW.primary_key is to actually use the value in the variable.
Here is the example of anonymous pl/pgsql block which doing something what you want:
do $$
declare
v pg_database = (pg_database) from pg_database where datname = 'template1';
fname text = 'datname';
n text;
begin
n := to_jsonb(v)->>fname;
raise info '%', n;
end $$;
Output:
INFO: template1
It is working example. In your trigger function it could be something like
declare
pk_name text;
pk_value text;
begin
pk_name := getprimarykey(TG_TABLE_NAME::VARCHAR(50));
pk_value := to_jsonb(NEW) ->> pk_name;
-- Do what you want with pk_value here
return null;
end $$;

How to know the date of modification of an SP?

Is it possible to know the date of modification and / or creation of an SP in PostgreSQL 9.4?
I need to identify them to upload them next Deploy.-
PostgreSQL has not this functionality. You can create own table and update it from event triggers.
create table updates(proc regprocedure primary key, t timestamp);
create or replace function event_trigger_for_ddl_command_end()
returns event_trigger as $$
declare obj record;
begin
for obj in select * from pg_event_trigger_ddl_commands()
loop
if obj.classid = 'pg_proc'::regclass then
insert into updates values(obj.objid, current_timestamp)
on conflict (proc) do update set t = current_timestamp
where updates.proc = excluded.proc;
end if;
end loop;
end;
$$ language plpgsql;
create event trigger trigger_for_ddl_command_end
on ddl_command_end
execute procedure event_trigger_for_ddl_command_end();
create or replace function fx(a int) returns int as $$ select 1 $$ language sql;
postgres=# select * from updates ;
+-------------+----------------------------+
| proc | t |
+-------------+----------------------------+
| fx(integer) | 2017-11-22 14:21:11.367036 |
+-------------+----------------------------+
(1 row)
-- alternative code without INSERT ON CONFLICT
create or replace function event_trigger_for_ddl_command_end()
returns event_trigger as $$
declare obj record;
begin
for obj in select * from pg_event_trigger_ddl_commands()
loop
if obj.classid = 'pg_proc'::regclass then
begin
update updates set t = current_timestamp
where proc = obj.objid;
if not found then
begin
insert into updates values(obj.objid, current_timestamp);
exception when unique_violation then
update updates set t = current_timestamp
where proc = obj.objid;
end;
end if;
end if;
end loop;
end;
$$ language plpgsql;

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

how to get the affected base table row count in a statement level trigger

I have these tables:
CREATE EXTENSION citext;
CREATE EXTENSION "uuid-ossp";
CREATE TABLE cities
(
city_id serial PRIMARY KEY,
city_name citext NOT NULL UNIQUE
);
INSERT INTO cities(city_name) VALUES
('New York'), ('Paris'), ('Madrid');
CREATE TABLE etags
(
etag_name varchar(128) PRIMARY KEY,
etag_value uuid
);
INSERT INTO etags(etag_name, etag_value)
VALUES ('cities', uuid_generate_v4());
I want to update the cities etag when the cities table changes. If no rows are affected by the insert, update or delete statement, I'd like to avoid to change the cities etag, so I wrote the following statement level trigger:
CREATE OR REPLACE FUNCTION update_etag()
RETURNS trigger AS
$BODY$
DECLARE
record_count integer;
vetag_name varchar(128);
BEGIN
GET DIAGNOSTICS record_count = ROW_COUNT;
vetag_name := TG_ARGV[0];
RAISE NOTICE 'affected %:%', vetag_name, record_count;
IF record_count = 0 THEN
RETURN NULL;
END IF;
UPDATE etags SET etag_value = uuid_generate_v4()
WHERE etag_name = vetag_name;
RETURN null;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
CREATE TRIGGER update_cities_etag_trigger
AFTER INSERT OR UPDATE OR DELETE
ON cities
FOR EACH STATEMENT
EXECUTE PROCEDURE update_etag('cities');
However GET DIAGNOSTICS record_count = ROW_COUNT; doesn't work for me, as it always returns 0.
If I execute the following:
DELETE FROM cities;
The following is output:
NOTICE: affected cities:0 Query returned successfully: 3 rows
affected, 47 msec execution time.
Is there a way to figure out how many rows are affected by the statement that triggers the trigger in a PostgreSQL statement-level trigger?
Version 10
CREATE TRIGGER
...
[ REFERENCING { { OLD | NEW } TABLE [ AS ] transition_relation_name } [ ... ] ]
...
https://www.postgresql.org/docs/current/static/release-10.html
Add AFTER trigger transition tables to record changed rows (Kevin
Grittner, Thomas Munro)
Transition tables are accessible from triggers written in server-side
languages.
Example
Solves it:
CREATE OR REPLACE FUNCTION update_etag()
RETURNS trigger AS
$BODY$
DECLARE
record_count integer;
vetag_name varchar(128);
begin
IF (TG_OP = 'DELETE') or (TG_OP = 'UPDATE') THEN
select count(*) from oldtbl into record_count ;
ELSE
select count(*) from newtbl into record_count ;
END IF;
vetag_name := TG_ARGV[0];
RAISE NOTICE 'affected %:%:%', vetag_name,TG_OP, record_count;
IF record_count = 0 THEN
RETURN NULL;
END IF;
UPDATE etags SET etag_value = uuid_generate_v4()
WHERE etag_name = vetag_name;
RETURN null;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
CREATE TRIGGER update_ins_cities_etag_trigger
AFTER INSERT
ON cities
REFERENCING NEW TABLE AS newtbl
FOR EACH STATEMENT
EXECUTE PROCEDURE update_etag('cities');
CREATE TRIGGER update_upd_cities_etag_trigger
AFTER UPDATE
ON cities
REFERENCING OLD TABLE AS oldtbl
FOR EACH STATEMENT
EXECUTE PROCEDURE update_etag('cities');
CREATE TRIGGER update_del_cities_etag_trigger
AFTER DELETE
ON cities
REFERENCING OLD TABLE AS oldtbl
FOR EACH STATEMENT
EXECUTE PROCEDURE update_etag('cities');
so=# INSERT INTO cities(city_name) VALUES
so-# ('New York'), ('Paris'), ('Madrid');
NOTICE: affected cities:INSERT:3
INSERT 0 3
so=# select * from etags;
etag_name | etag_value
-----------+--------------------------------------
cities | dc7d1525-eea7-4822-b736-5141a20764f8
(1 row)
so=# insert into cities(city_name) values ('Budapest');
NOTICE: affected cities:INSERT:1
INSERT 0 1
so=# select * from etags;
etag_name | etag_value
-----------+--------------------------------------
cities | df835f44-dada-4a94-bb62-5890f2316103
(1 row)
so=# delete from cities where city_id > 42;
NOTICE: affected cities:DELETE:0
DELETE 0
so=# select * from etags;
etag_name | etag_value
-----------+--------------------------------------
cities | df835f44-dada-4a94-bb62-5890f2316103
(1 row)