PLPGSQL Cascading Triggers? - sql

I am trying to create a trigger, so that when ever I add a new record it adds another record in the same table. The session field will only take values between 1 and 4. So when I add a 1 in session I want it to add another record but with session 3 blocked. But the problem is that it leads to cascading triggers and it inserts itself again and again because the trigger is triggered when inserted.
I have for example a simple table:
CREATE TABLE example
(
id SERIAL PRIMARY KEY
,name VARCHAR(100) NOT NULL
,session INTEGER
,status VARCHAR(100)
);
My trigger function is:
CREATE OR REPLACE FUNCTION add_block() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO example VALUES (NEW.id + 1, NEW.name, NEW.session+2, 'blocked');
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
Trigger is:
CREATE TRIGGER add_block
AFTER INSERT OR UPDATE
ON example
FOR EACH ROW
EXECUTE PROCEDURE add_block();
I get error:
SQL statement "INSERT INTO example VALUES ( $1 +1, $2 , $3 + 2, $4)"
PL/pgSQL function "add_block" line 37 at SQL statement
This error repeats itself so many times that I can't see the top.
How would I solve this?
EDIT:
CREATE TABLE block_rules
(
id SERIAL PRIMARY KEY
,session INTEGER
,block_session INTEGER
);
This table holds the block rules. So if a new record is inserted into the EXAMPLE table with session 1 then it blocks session 3 accordingly by inserting a new record with blocked status in the same (EXAMPLE) table above (not block_rules). Same for session 2 but it blocks session 4.
The block_rules table holds the rules (or pattern) to block a session by. It holds
id | session | block_session
------------------------------
1 | 1 | 3
2 | 2 | 4
3 | 3 | 2
How would I put that in the WHEN statement of the trigger going with Erwin Branstetter's answer below?
Thanks

New answer to edited question
This trigger function adds blocked sessions according to the information in table block_rules.
I assume that the tables are linked by id - information is missing in the question.
I now assume that the block rules are general rules for all sessions alike and link by session. The trigger is only called for non-blocked sessions and inserts a matching blocked session.
Trigger function:
CREATE OR REPLACE FUNCTION add_block()
RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO example (name, session, status)
VALUES (NEW.name
,(SELECT block_session
FROM block_rules
WHERE session = NEW.session)
,'blocked');
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
Trigger:
CREATE TRIGGER add_block
AFTER INSERT -- OR UPDATE
ON example
FOR EACH ROW
WHEN (NEW.status IS DISTINCT FROM 'blocked')
EXECUTE PROCEDURE add_block();
Answer to original question
There is still room for improvement. Consider this setup:
CREATE OR REPLACE FUNCTION add_block()
RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO example (name, session, status)
VALUES (NEW.name, NEW.session + 2, 'blocked');
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
CREATE TRIGGER add_block
AFTER INSERT -- OR UPDATE
ON example
FOR EACH ROW
WHEN (NEW.session < 3)
-- WHEN (status IS DISTINCT FROM 'blocked') -- alternative guess at filter
EXECUTE PROCEDURE add_block();
Major points:
For PostgreSQL 9.0 or later you can use a WHEN condition in the trigger definition. This would be most efficient. For older versions you use the same condition inside the trigger function.
There is no need to add a column, if you can define criteria to discern auto-inserted rows. You did not tell, so I assume that only auto-inserted rows have session > 2 in my example. I added an alternative WHEN condition for status = 'blocked' as comment.
You should always provide a column list for INSERTs. If you don't, later changes to the table may have unexpected side effects!
Do not insert NEW.id + 1 in the trigger manually. This won't increment the sequence and the next INSERT will fail with a duplicate key violation.
id is a serial column, so don't do anything. The default nextval() from the sequence is inserted automatically.
Your description only mentions INSERT, yet you have a trigger AFTER INSERT OR UPDATE. I cut out the UPDATE part.
The keyword plpgsql doesn't have to be quoted.

OK so can't you just add another column, something like this:
ALTER TABLE example ADD COLUMN trig INTEGER DEFAULT 0;
CREATE OR REPLACE FUNCTION add_block() RETURNS TRIGGER AS $$
BEGIN
IF NEW.trig = 0 THEN
INSERT INTO example VALUES (NEXTVAL('example_id_seq'::regclass), NEW.name, NEW.session+2, 'blocked', 1);
END IF;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
it's not great, but it works :-)

CREATE OR REPLACE FUNCTION add_block() RETURNS TRIGGER AS $$
BEGIN
SET SESSION session_replication_role = replica;
INSERT INTO example VALUES (NEXTVAL('example_id_seq'::regclass), NEW.name, NEW.session+2, 'blocked');
SET SESSION session_replication_role = origin;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';

Related

Trigger postgreSQL

So. I tried made a trigger for updating quality medicament( i take quality from delivery.quality and put in medicaments.quality)
create or replace function addedmed()
returns trigger
language plpgsql
as
$$
begin
update medicaments set medicaments.quality = delivery.quality + medicaments.quality
where medicaments.id_med = delivery.id_med_del;
end;
$$;
and then - i try insert data to delivery
insert into delivery(id_del,id_provider,date_of_get,id_worker_del,id_med_del,quality)
values (default, 3 , current_timestamp ,1 ,6 ,10);
and then i have a exception -
ЗАПРОС: update medicaments set medicaments.quality = delivery.quality + medicaments.quality
where medicaments.id_med = delivery.id_med_del
КОНТЕКСТ: PL/pgSQL function addedmed() line 3 at SQL statement
so, pls help me, because i dont know how resolve it
This is not valid trigger function code. If you want to refer to the row being inserted in delivery, then use pseudo-table new, not delivery.
create or replace function addedmed()
returns trigger
language plpgsql
as $$
begin
update medicaments set quality = quality + new.quality where id_med = new.id_med_del;
return new;
end;
$$;
Note that the trigger returns the new record. You can then actually create the trigger as follows:
create trigger trg_addedmed
before insert on delivery
for each row
execute addedmed()
;
I defined a before insert trigger, you can change that to after insert if you prefer (in that case you can return null instead of new).

Trigger to update a different table

Using Postgres 9.4, I have 2 tables streams and comment_replies. I am trying to do is update the streams.comments count each time a new comment_replies is inserted to keep track of the number of comments a particular stream has. I am not getting any errors but when I try to create a new comment it gets ignored.
This is how I am setting up my trigger. stream_id is a foreign key, so every stream_id will correspond to a streams.id which is the primary key of the streams table. I have been looking at this example: Postgres trigger function, but haven't been able to get it to work.
CREATE TABLE comment_replies (
id serial NOT NULL PRIMARY KEY,
created_on timestamp without time zone,
comments text,
profile_id integer,
stream_id integer
);
The trigger function:
CREATE OR REPLACE FUNCTION "Comment_Updates"()
RETURNS trigger AS
$BODY$BEGIN
update streams set streams.comments=streams.comments+1
where streams.id=comment_replies_streamid;
END$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
And the trigger:
CREATE TRIGGER comment_add
BEFORE INSERT OR UPDATE
ON comment_replies
FOR EACH ROW
EXECUTE PROCEDURE "Comment_Updates"();
How can I do this?
There are multiple errors. Try instead:
CREATE OR REPLACE FUNCTION comment_update()
RETURNS trigger AS
$func$
BEGIN
UPDATE streams s
SET streams.comments = s.comments + 1
-- SET comments = COALESCE(s.comments, 0) + 1 -- if the column can be NULL
WHERE s.id = NEW.streamid;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER comment_add
BEFORE INSERT OR UPDATE ON comment_replies -- on UPDATE, too? Really?
FOR EACH ROW EXECUTE PROCEDURE comment_update();
You need to consider DELETE as well if that is possible. Also if UPDATE can change stream_id. But why increase the count for every UPDATE? This looks like another error to me.
It's a syntax error to table-qualify the target column in the SET clause of UPDATE.
You need to return NEW in a BEFORE trigger unless you want to cancel the INSERT / UPDATE.
Or you make it an AFTER trigger, which would work for this, too.
You need to reference NEW for the stream_id of the current row (which is automatically visible inside the trigger function.
If streams.comments can be NULL, use COALESCE.
And rather use unquoted, legal, lower-case identifiers.

Trigger function to delete certain rows from the same table

I'm trying to create a Trigger/Function in Postgres that will check, upon an insert to a table, whether or not there is already another post by a different member with the same content. If there is a post, this function will not insert the new one and leave the table unchanged. Otherwise, it will be added.
So far, the trigger and function look like:
Trigger:
CREATE TRIGGER isPostUnique
AFTER INSERT ON posts
FOR EACH ROW
EXECUTE PROCEDURE deletePost();
Function:
CREATE FUNCTION deletePost() RETURNS TRIGGER AS $isPostUnique$
BEGIN
IF (EXISTS (SELECT * FROM posts p1, posts p2
WHERE (p1.userID <> p2.userID)
AND (p1.content LIKE p2.content)))
THEN
DELETE FROM NEW WHERE (posts.postID = NEW.postID);
RETURN NEW;
END IF;
END;
$isPostUnique$ LANGUAGE plpgsql;
Adding the function and trigger works without any errors, but when I try to run the following query to test it: INSERT INTO posts VALUES (7, 3, 'test redundant post', 10, 1); I get this error
ERROR: relation "new" does not exist
LINE 1: DELETE FROM NEW WHERE (posts.postID = NEW.postID)
^
QUERY: DELETE FROM NEW WHERE (posts.postID = NEW.postID)
CONTEXT: PL/pgSQL function dp() line 7 at SQL statement
I am aware that you can't use 'NEW' in FOR EACH ROW inserts, but I have no other idea of how to accomplish this.
Updated answer for updated question
Of course you can use NEW in FOR EACH ROW trigger function. You just can't direct a DELETE statement at it. It's a row type (data type HeapTuple to be precise), not a table.
To abort the INSERT silently (no exception raised) if the same content is already there ...
CREATE FUNCTION deletePost()
RETURNS TRIGGER AS
$func$
BEGIN
IF EXISTS (
SELECT 1
FROM posts p
WHERE p.content = NEW.content
-- AND p.userID <> NEW.userID -- I doubt you need this, too?
) THEN
RETURN NULL; -- cancel INSERT
ELSE
RETURN NEW; -- go ahead
END IF;
END
$func$ LANGUAGE plpgsql;
Of course this only works for a trigger ...
...
BEFORE INSERT ON posts
...
Unique index
A UNIQUE constraint or a unique index (almost the same effect) might be a superior solution:
CREATE UNIQUE INDEX posts_content_uni_idx (content);
Would raise an exception at the attempt to insert a duplicate value. No trigger necessary.
It also provides the very well needed index to speed up things.

How do you ensure that a table is only updated when a new modified_at is greater than the current modified_at in PostgreSQL?

I have a situation where I want to deny an update to a table if the current modified_at column is more recent than the one specified in the update. I attempted to do this with a CHECK constraint, but it has no effect.
Here is an example:
CREATE TABLE test (
id SERIAL PRIMARY KEY,
value INTEGER NOT NULL DEFAULT 0,
modified_at timestamptz DEFAULT NOW()
);
ALTER TABLE test ADD CHECK (modified_at >= modified_at);
INSERT INTO test (id, value) VALUES (1, 1);
INSERT 0 1
SELECT * FROM test;
id | value | modified_at
----+-------+-------------------------------
1 | 1 | 2013-05-30 14:34:37.234456-07
UPDATE test
SET value = 2, modified_at = NOW() - INTERVAL '1 day'
WHERE id = 1;
UPDATE 1
SELECT * FROM test;
id | value | modified_at
----+-------+-------------------------------
1 | 2 | 2013-05-29 14:35:41.337543-07
This doesn't appear to work as expected. Intuitively I could see this being a problem. How does the planner know that the left hand side should be the new value and the right hand side should be the old?
Knowing this doesn't work, how should this constraint be enforced?
You will have to check old modified_date against new modified_date of the updated row, and you can do this using triggers.
Set the trigger to run on each row before update and create a function that deals with it, choosing if you want to keep the old modified_date or if you don't want to perform any update at all.
The trigger can be done like this:
CREATE TRIGGER trigger_test
BEFORE INSERT OR UPDATE
ON test
FOR EACH ROW
EXECUTE PROCEDURE fn_trigger_test();
And the function like this:
CREATE OR REPLACE FUNCTION fn_trigger_test()
RETURNS trigger AS
$BODY$
DECLARE
BEGIN
IF (TG_OP = 'UPDATE') THEN
IF NEW.modified_at<OLD.modified_at THEN
RAISE EXCEPTION 'Date_modified older than previous';
/* or to keep the old modified date:
NEW.modifed_at=OLD.modified_at;
RETURN NEW; */
ELSE
RETURN NEW;
END IF;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
As far as CHECK constraints are concerned, there's no such thing as new values and old values.
PostgreSQL triggers have access to new data. Another fairly common approach is to revoke permissions on the tables, and require all access to take place through stored procedures. Stored procedures can have access to new data, passed through parameters, and stored procs can check values in tables, update additional tables for auditing, etc. See CREATE FUNCTION and plpgsql.

Trigger to delete past records in postgresql

I want only to maintain present 1 month records log details. need to delete past record log details.I tried this code however could not work this,
create sequence userseq1;
CREATE TABLE users
( id integer NOT NULL DEFAULT nextval('userseq1'::regclass)
);
INSERT INTO users VALUES(126);
CREATE TABLE History
( userid integer
, createdate timestamp
);
CREATE OR REPLACE FUNCTION recordcreatetime() RETURNS trigger language plpgsql
AS $$
DECLARE
BEGIN
INSERT INTO History values(new.id,NOW() );
RETURN NEW;
END;
$$;
CREATE TRIGGER user_hist
BEFORE INSERT ON users
FOR EACH ROW
EXECUTE procedure recordcreatetime();
However it is working to insert values sequencing one by one adding.I want to delete the previous 1 month record Log details.I tried this below code and it is not working
CREATE OR REPLACE FUNCTION trf_keep_row_number_steady()
RETURNS TRIGGER AS
$body$
DECLARE
BEGIN
IF (SELECT count(createdate) FROM history) > rownum_limit
THEN
DELETE FROM history
WHERE createdate = (SELECT min(createdate) FROM history);
END IF;
END;
$body$
LANGUAGE plpgsql;
CREATE TRIGGER tr_keep_row_number_steady
AFTER INSERT ON history
FOR EACH ROW EXECUTE PROCEDURE trf_keep_row_number_steady();
I can see in your second code block, you have a trigger on history table and you are trying to DELETE FROM history in that same trigger.
Insert / Update / Delete on a table through a trigger on the same table is not allowed. Please think of some other alternative, e.g., running a separate DELETE statement for the required cleanup of rows before or after your main INSERT statement.