I am having a problem with a trigger. I created a trigger and a function to keep track of how many rows are in use by each user in my DB. The INSERT part of the trigger and function work correctly, but the DELETE does nothing. When I insert rows in my app, the rowcount goes up, when I delete, the rowcount does not change.
Here is the TABLE where I keep the row count:
Table "public.rowcount"
Column | Type | Modifiers
------------+---------+-----------
user__id | integer | not null
table_name | text | not null
total_rows | bigint |
Here is my TRIGGER:
CREATE TRIGGER countrows_m_time
AFTER INSERT OR DELETE on m_time
FOR EACH ROW EXECUTE PROCEDURE count_rows_m_time();
And here is the FUNCTION:
CREATE OR REPLACE FUNCTION count_rows_m_time()
RETURNS TRIGGER AS
'
BEGIN
IF TG_OP = ''INSERT'' THEN
UPDATE rowcount
SET total_rows = total_rows + 1
WHERE table_name = TG_RELNAME
AND user__id = (SELECT user__id from vi_m_time_users where m_value_id = NEW.m_value_id);
ELSIF TG_OP = ''DELETE'' THEN
UPDATE rowcount
SET total_rows = total_rows - 1
WHERE table_name = TG_RELNAME
AND user__id = (SELECT user__id from vi_m_time_users where m_value_id = OLD.m_value_id);
END IF;
RETURN NULL;
END;
' LANGUAGE plpgsql;
Any ideas?
Many thanks,
Shea
After my original post, I went back to simplify the problem. I split the TRIGGER and FUNCTION into separate INSERT and DELETE activities. Both the INSERT TRIGGER and FUNCTION continue to work correctly as an AFTER trigger. So I have excluded it from the post. Here is a simplified problem with the DELETE trigger.
Here is my new TRIGGER:
CREATE TRIGGER remrows_m_int
BEFORE DELETE on m_int
FOR EACH ROW EXECUTE PROCEDURE rem_rows_m_int();
And here is my new FUNCTION:
CREATE OR REPLACE FUNCTION rem_rows_m_int()
RETURNS TRIGGER AS
'
BEGIN
IF TG_OP = ''DELETE'' THEN
UPDATE rowcount
SET total_rows = total_rows - 1
WHERE table_name = TG_RELNAME
AND user__id = (SELECT user__id from vi_m_int_users where result_id = OLD.result__id);
END IF;
RETURN OLD;
END;
' LANGUAGE plpgsql;
This trigger is now working if I remove rows on the m_int table. The problem with the AFTER trigger was never resolved, but using a BEFORE with a RETURN OLD seems to be an ok substitute. By hard coding certain variables, the issue was related to the use of the OLD.result__id in the function.
Returning NULL cancels the INSERT/DELETE operations. (You can use it for referential integrity too complicated to be enforced with a simple constraint.)
You want to return OLD from the DELETE and NEW from the INSERT.
CREATE OR REPLACE FUNCTION count_rows_m_time()
RETURNS TRIGGER AS $count_rows_m_time$
DECLARE
BEGIN
IF (TG_OP = 'INSERT') THEN
UPDATE rowcount
SET total_rows = (total_rows + 1)
WHERE table_name = TG_RELNAME
AND user_id = (SELECT user_id FROM vi_m_time_users WHERE m_value_id = NEW.m_value_id);
ELSIF (TG_OP = 'DELETE') THEN
UPDATE rowcount
SET total_rows = (total_rows - 1)
WHERE table_name = TG_RELNAME
AND user_id = (SELECT user_id FROM vi_m_time_users WHERE m_value_id = OLD.m_value_id);
END IF;
RETURN NULL;
END;
$count_rows_m_time$ LANGUAGE plpgsql;
*i think so
Related
I came across a strange behavior (at least for me) with PostgreSQL and a trigger BEFORE UPDATE.
I have a table witch has an updated_at column witch is set by a BEFORE UPDATE trigger.
I need to add new columns to this table and set their values with an UPDATE query (not with DEFAULT).
It works just fine excepts when i do an UPDATE juste before adding those columns.
Here's an example :
ALTER TABLE my_schema.my_table ADD COLUMN new_column varchar(50);
UPDATE my_schema.my_table SET new_column = 'new_column_update' WHERE id = xxxxxx;
This script works fine.
But if i do an UPDATE before :
UPDATE my_schema.my_table SET other_column = 'other_column_update' WHERE id = xxxxxx; -- the TRIGGER is triggered
ALTER TABLE my_schema.my_table ADD COLUMN new_column varchar(50);
UPDATE my_schema.my_table SET new_column = 'new_column_update' WHERE id = xxxxxx; -- this UPDATE does't do anything
It doesn't works anymore.
After a few (a lot) hours, i found that the trigger BEFORE UPDATE is reponsible. But i can't find why.
I found a workaround by temporary disabling the trigger
ALTER TABLE my_table DISABLE TRIGGER update_date;
Here is a dbfiddle, just run it to see this behaviour :
dbfiddle
Here is the code in dbfiddle
CREATE TABLE my_table (
other_column varchar(50),
updated_at timestamp
);
CREATE OR REPLACE FUNCTION update_date()
RETURNS trigger
LANGUAGE plpgsql
COST 1
AS '
BEGIN
IF row(NEW.*) IS DISTINCT FROM row(OLD.*) THEN
NEW.updated_at = now();
RETURN NEW;
ELSE
RETURN OLD;
END IF;
END;
'
;
CREATE TRIGGER update_date BEFORE
UPDATE
ON
my_table FOR EACH ROW EXECUTE PROCEDURE update_date();
INSERT INTO my_table VALUES ('other_column_insert');
UPDATE my_table SET other_column = 'other_column_update';
ALTER TABLE my_table ADD COLUMN new_colum varchar(50);
UPDATE my_table SET new_colum = 'new_colum_update'; -- this UPDATE doesn't work because of the trigger BEFORE UPDATE
-- It is possible to make it works by disabling the trigger BEFORE the first UPDATE
-- ALTER TABLE my_table DISABLE TRIGGER update_date;
Have you ever encountered this behavior ?
It's something to do with the (unnecessary) wrapping of NEW/OLD with a ROW(...) constructor:
BEGIN
IF row(NEW.*) IS DISTINCT FROM row(OLD.*) THEN
-- IF NEW IS DISTINCT FROM OLD THEN
NEW.updated_at = now();
ELSE
RAISE EXCEPTION $$NOT DISTINCT: % / %$$, NEW, OLD;
END IF;
RETURN NEW;
END;
I've also moved the RETURN NEW to the end. If you try your version you should see the exceptions. If you replace it out with the commented-out one below then it works.
Now, as to why this is failing when you compare rows I'm not sure and it's too hot and late on a Friday afternoon where I am to figure it out I'm afraid.
I am going to say this is a caching problem. I modified the function to see what is going on:
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table (
other_column varchar(50),
updated_at timestamp
);
CREATE OR REPLACE FUNCTION public.update_date()
RETURNS trigger
LANGUAGE plpgsql
COST 1
AS $function$
BEGIN
RAISE NOTICE 'New row %', ROW(NEW.*);
RAISE NOTICE 'Old row%', ROW(OLD.*);
RAISE NOTICE 'New.* %', (NEW.*)::text;
RAISE NOTICE 'Old.* %', (OLD.*)::text;
IF NEW.* IS DISTINCT FROM OLD.* THEN
NEW.updated_at = now();
RETURN NEW;
ELSE
RETURN OLD;
END IF;
END;
$function$;
CREATE TRIGGER update_date BEFORE
UPDATE
ON
my_table FOR EACH ROW EXECUTE PROCEDURE update_date();
INSERT INTO my_table VALUES ('other_column_insert');
UPDATE my_table SET other_column = 'other_column_update';
NOTICE: New row (other_column_update,)
NOTICE: Old row(other_column_insert,)
NOTICE: New.* (other_column_update,)
NOTICE: Old.* (other_column_insert,)
ALTER TABLE my_table ADD COLUMN new_colum varchar(50);
UPDATE my_table SET new_colum = 'new_colum_update';
NOTICE: New row (other_column_update,"2022-08-12 10:38:54.815831")
NOTICE: Old row(other_column_update,"2022-08-12 10:38:54.815831")
NOTICE: New.* (other_column_update,"2022-08-12 10:38:54.815831",new_colum_update)
NOTICE: Old.* (other_column_update,"2022-08-12 10:38:54.815831",)
It has to do with the ROW(). Even doing ROW(NEW.*)::my_table or using EXECUTE to make the query dynamic and not use caching does not work.
I have a query which updates the records based on variables old_id and new_id. But condition is I need to fetch the variables dynamically. Here is simple query which I am using.
do
$$
declare
old_id bigint = 1561049391647687270;
declare new_id bigint = 2068236279446765699;
begin
update songs set poet_id = new_id where poet_id = old_id;
update poets set active = true where id = new_id;
update poets set deleted = true where id = old_id;
end
$$;
I need to assign the old_id and new_id dynamically
do
$$
declare
su record;
pc record;
old_id bigint;
new_id bigint;
begin
for pc in select name, count(name)
from poets
where deleted = false
group by name
having count(name) > 1
order by name
loop
for su in select * from poets where name ilike pc.name
loop
-- old_id could be null where I have 2 continue the flow without update
for old_id in (select id from su where su.link is null)
loop
raise notice 'old: %', old_id;
end loop;
-- new_id could be more than 2 skip this condition as well
for new_id in (select id from su where su.link is not null)
loop
raise notice 'new: %', new_id;
end loop;
end loop;
-- run the statement_1 example if new_id and old_id is not null
end loop;
end
$$;
The expected problem statement (to assign variable and use it in further execution) is with in comment.
(a) In your first "simple query", the update of the table poets could be automatically executed by a trigger function defined on the table songs :
CREATE OR REPLACE FUNCTION songs_update_id ()
RETURNS trigger LANGUAGE plpgsql AS
$$
BEGIN
UPDATE poets SET active = true WHERE id = NEW.poet_id ;
UPDATE poets SET deleted = true WHERE id = OLD.poet_id ; -- SET active = false to be added ?
END ;
$$ ;
CREATE OR REPLACE TRIGGER songs_update_id AFTER UPDATE OF id ON songs
FOR EACH ROW EXECUTE songs_update_id () ;
Your first query can then be reduced as :
do
$$
declare
old_id bigint = 1561049391647687270;
declare new_id bigint = 2068236279446765699;
begin
update songs set poet_id = new_id where poet_id = old_id;
end
$$;
(b) The tables update could be performed with a sql query instead of a plpgsql loop and with better performances :
do
$$
BEGIN
UPDATE songs
SET poet_id = list.new_id[1]
FROM
( SELECT b.name
, array_agg(b.id) FILTER (WHERE b.link IS NULL) AS old_id
, array_agg(b.id) FILTER (WHERE b.link IS NOT NULL) AS new_id
FROM
( SELECT name
FROM poets
WHERE deleted = false
GROUP BY name
HAVING COUNT(*) > 1
-- ORDER BY name -- this ORDER BY sounds like useless and resource-intensive
) AS a
INNER JOIN poets AS b
ON b.name ilike a.name
GROUP BY b.name
HAVING array_length(old_id, 1) = 1
AND array_length(new_id, 1) = 1
) AS list
WHERE poet_id = list.old_id[1] ;
END ;
$$;
This solution is not tested yet and could have to be adjusted in order to work correctly. Please provide the tables definition of songs and poets and a sample of data in dbfiddle so that I can test and adjust the proposed solution.
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
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)
Just need help as to debug some syntax errors in the below code.The code is as below and have only couple of syntax errors near keywords like Insert, Select, etc
CREATE OR REPLACE FUNCTION audit_temp() RETURNS TRIGGER LANGUAGE plpgsql AS $BODY$
DECLARE
ri RECORD;
oldValue TEXT;
newValue TEXT;
isColumnSignificant BOOLEAN;
isValueModified BOOLEAN;
BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
NEW.record_modified_ = clock_timestamp();
FOR ri IN
-- Fetch a ResultSet listing columns defined for this trigger's table.
SELECT ordinal_position, column_name, data_type
FROM information_schema.columns
WHERE table_schema = quote_ident(TG_TABLE_SCHEMA)
AND table_name = quote_ident(TG_TABLE_NAME)
ORDER BY ordinal_position
LOOP
-- For each column in this trigger's table, copy the OLD & NEW values into respective variables.
-- NEW value
EXECUTE 'SELECT ($1).' || ri.column_name || '::text' INTO newValue USING NEW;
-- OLD value
IF TG_OP = 'INSERT' THEN -- If operation is an INSERT, we have no OLD value, so use an empty string.
oldValue := ''::varchar;
ELSE -- Else operation is an UPDATE, so capture the OLD value.
EXECUTE 'SELECT ($1).' || ri.column_name || '::text' INTO oldValue USING OLD;
END IF;
isColumnSignificant := (position( '_x_' in ri.column_name ) < 1) AND
(ri.column_name <> 'pkey_') AND
(ri.column_name <> 'record_modified_');
IF isColumnSignificant THEN
isValueModified := oldValue <> newValue; -- If this nthField in the table was modified, make history.
IF isValueModified THEN
/*RAISE NOTICE E'Inserting history_ row for INSERT or UPDATE.\n';*/
INSERT INTO audit_temp( operation_, table_oid_, table_name_, uuid_, column_name_, ordinal_position_of_column_, old_value_, new_value_ )
VALUES ( TG_OP, TG_RELID, TG_TABLE_NAME, NEW.pkey_, ri.column_name::VARCHAR, ri.ordinal_position, oldValue::VARCHAR, newValue::VARCHAR);
END IF;
END IF;
END LOOP;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
/*RAISE NOTICE E'Inserting history_ row for DELETE.\n';*/
-- Similar to INSERT above, but refers to OLD instead of NEW, and passes empty values for last 4 fields.
INSERT INTO audit_temp ( operation_, table_oid_, table_name_, uuid_, column_name_, ordinal_position_of_column_, old_value_, new_value_ )
VALUES ( TG_OP, TG_RELID, TG_TABLE_NAME, OLD.pkey_, ''::VARCHAR, 0, ''::VARCHAR, ''::VARCHAR );
RETURN OLD;
END IF;
/* Should never reach this point. Branching in code above should always reach a call to RETURN. */
RAISE EXCEPTION 'Unexpectedly reached the bottom of this function without calling RETURN.';
END; $BODY$;
The error is as follows & mostly around Select Insert keywords only:
>[Error] Script lines: 1-42 -------------------------
ERROR: syntax error at or near "SELECT"
Any suggestions?????
Syntax error
The offending statement is this (and the others like it):
EXECUTE 'SELECT ($1).' || ri.column_name || '::text' INTO newValue USING NEW;
According to the documentation parameter symbols can only be used for data values — if you want to use dynamically determined table or column names, you must insert them into the command string textually. So the solution would be:
EXECUTE 'SELECT (' || NEW || ').' || ri.column_name || '::text' INTO newValue;
Improvements
You can make a few improvements to your trigger function to make it faster and more efficient:
You should check isColumnSignficant before you populate oldValue and newValue.
When updating, you do not have to record the OLD values: they are already in the audit table when the data was first inserted or later updated.
When deleting, don't store empty strings and 0, just leave the columns NULL.