Before Delete Trigger With Update Other Rows Postgres - sql

I have a table with a foreign key column referencing the same table. This is to do a grouping. I will have one group_master, and then every item in the table that is sufficiently similar to the group_master has a FK referencing that group_master id.
On delete, if the group_master is deleted, I need to update the group to have a new master so I use a Before delete trigger and in the trigger I identify the rows to be updated. Trigger and function code included below.
CREATE OR REPLACE FUNCTION assign_new_group_master()
RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
DECLARE
new_group_master details%ROWTYPE;
BEGIN
IF (SELECT COUNT(*) FROM details WHERE group_master_id = old.id AND old.id <> id) = 0
THEN RETURN old;
END IF;
SELECT * INTO new_group_master
FROM details
WHERE group_master_id = old.id AND old.id <> id
ORDER BY similarity_to_master DESC
LIMIT 1;
UPDATE details
SET group_master_id = new_group_master.id
WHERE group_master_id = old.id AND id <> old.id;
PERFORM pg_notify('group_master_id_updated'::Text,row_to_json(new_group_master)::text);
RETURN old;
END;
$$;
CREATE TRIGGER group_master_deleted_trigger
BEFORE DELETE ON details
FOR EACH ROW
WHEN (old.id = old.group_master_id)
EXECUTE FUNCTION assign_new_group_master();
This works great when deleting 1 detail element. The problem is if I try to delete many detail elements at 1, it is likely that I am deleting 2 items from the same group which causes this error to be thrown.
ERROR: tuple to be updated was already modified by an operation triggered by the current command
HINT: Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.
SQL state: 27000
As far as I understand, this is because 1 group_master gets deleted, all its children are updated, then the new group_master gets deleted and so all its children need to be updated again which is not allowed.
How can I get around this issue? I can not change to an After trigger as the hint suggests because then I would be violating the FK constraints of the group_master_id on the children.
Thanks

Related

PL/SQL ORACLE: Trigger updates on delete

I am trying to make a trigger that increases the booked value and decreases the available value whenever new record is inserted inside the table ticket_price. If a record is deleted, I want it to decrease the booked value and increase the available value.
Although I am able to successfully make the trigger work for INSERT, I am unable to do the same for updating the values on deletion of a record.T his is the error I get whenever I try to delete a record
ORA-01403: no data found
ORA-06512: at "K.CAL", line 6
ORA-04088: error during execution of trigger 'K.CAL'
Just to clarify, I am updating values in another table, not the same table I am deleting!
Here is my code for the trigger:
CREATE OR REPLACE TRIGGER cal
BEFORE INSERT OR DELETE ON TICKET_PRICE FOR EACH ROW
DECLARE
V_TICKET TICKET_PRICE.TICKETPRICE%TYPE;
V_BOOKED FLIGHTSEAT.BOOKED_SEATS%TYPE;
V_AVAILABLE FLIGHTSEAT.AVAILABLE_SEATS%TYPE;
BEGIN
SELECT BOOKED_SEATS,AVAILABLE_SEATS
INTO V_BOOKED,V_AVAILABLE
FROM FLIGHTSEAT
WHERE SEAT_ID=:NEW.SEAT_ID;
IF INSERTING THEN
V_BOOKED:=V_BOOKED+1;
V_AVAILABLE:=V_AVAILABLE-1;
UPDATE FLIGHTSEAT
SET BOOKED_SEATS=V_BOOKED, AVAILABLE_SEATS=V_AVAILABLE
WHERE SEAT_ID=:NEW.SEAT_ID;
ELSIF DELETING THEN
V_BOOKED:=V_BOOKED-1;
V_AVAILABLE:=V_AVAILABLE+1;
UPDATE FLIGHTSEAT
SET BOOKED_SEATS=V_BOOKED, AVAILABLE_SEATS=V_AVAILABLE
WHERE SEAT_ID=1;
END IF;
END;
You have correctly surmised that :new.seat is not available on the update for a delete. But neither is it available for the select and ow could you know sea_id=1 was need to be updated? For reference to Deleted row data use :Old.column name; is this case use :old_seat_id for both select and update.
But you don't need the select at all. Note: Further you have an implied assumption that seat_id is unique. I'll accept that below.
create or replace trigger cal
before insert or delete on ticket_price
for each row
declare
v_seat_id flightseat.seat_id%type;
v_booked flightseat.booked_seats%type;
begin
if INSERTING then
v_booked := 1;
v_seat_id := :new.seat_id;
else
v_booked := -1;
v_seat_id := :old.seat_id;
end if;
update flightseat
set booked_seats=booked_seats+v_booked
, available_seats=available_seats-v_booked
where seat_id = v_seat_id;
end;

BEFORE DELETE trigger

How to stop from deleting a row, that has PK in another table (without FK) with a trigger?
Is CALL cannot_delete_error would stop from deleting?
This is what I've got so far.
CREATE TRIGGER T1
BEFORE DELETE ON Clients
FOR EACH ROW
BEGIN
SELECT Client, Ref FROM Clients K, Invoice F
IF F.Client = K.Ref
CALL cannot_delete_error
END IF;
END
Use an 'INSTEAD OF DELETE' trigger.
Basically, you can evaluate whether or not you should the delete the item. In the trigger you can ultimately decide to delete the item like:
--test to see if you actually should delete it.
--if you do decide to delete it
DELETE FROM MyTable
WHERE ID IN (SELECT ID FROM deleted)
One side note, remember that the 'deleted' table may be for several rows.
Another side note, try to do this outside of the db if possible! Or with a preceding query. Triggers are downright difficult to maintain. A simple query, or function (e.g. dbo.udf_CanIDeleteThis()') can be much more versatile.
If you're using MySQL 5.5 or up you can use SIGNAL
DELIMITER //
CREATE TRIGGER tg_fk_check
BEFORE DELETE ON clients
FOR EACH ROW
BEGIN
IF EXISTS(SELECT *
FROM invoices
WHERE client_id = OLD.client_id) THEN
SIGNAL sqlstate '45000'
SET message_text = 'Cannot delete a parent row: child rows exist';
END IF;
END//
DELIMITER ;
Here is SQLFiddle demo. Uncomment the last delete and click Build Schema to see it in action.

Conditional CASCADE operation for foreign key constraint?

I have parent and child table where child has a FK pointing to the PK of parent table. When I delete something in parent table I can have child records deleted as well by having ON DELETE CASCADE.
However, in my parent table I don't delete records at all. Instead I set the column state = "passive". I want to delete related entries in the child table.
Do we have something like a "conditional CASCADE" in Postgres? Or is the solution to manually delete entries in the child table?
You would have to do this in a trigger that takes action ON UPDATE. Where the NEW.state = "passive", delete the child rows.
There is nothing like "conditional CASCADE". The closest thing that comes to mind would be to disable triggers. But that's not helpful in your case.
Assumptions:
- state is defined NOT NULL.
- parent_id never changes. If it does you'll want to cascade that UPDATE as well.
The condition to fire the trigger ON UPDATE should be:
NEW.state = "passive"
AND OLD.state <> "passive"
.. since you do not want to trigger it over and over again, only once when parent is set to "passive".
CREATE OR REPLACE FUNCTION trg_upbef()
RETURNS TRIGGER AS
$func$
BEGIN
DELETE FROM child
WHERE parent_id = OLD.parent_id; -- OLD works for UPDATE & DELETE
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Instead of checking the condition in the trigger function, you can do that in the trigger directly since Postgres 9.0, thereby saving a bit of overhead:
CREATE TRIGGER upd_cascade_del
BEFORE UPDATE ON parent
FOR EACH ROW
WHEN (NEW.state = "passive" AND
OLD.state <> "passive") -- parenthesis required
EXECUTE PROCEDURE trg_upbef();
Don't forget to add a trigger for ON DELETE as well. You don't normally DELETE, but if you do, you want to either raise an exception or cascade the operation.
Trigger function has to RETURN OLD, since NEW is not defined in this case. Otherwise the same:
CREATE OR REPLACE FUNCTION trg_delbef()
...
RETURN OLD;
...
$func$ LANGUAGE plpgsql;
CREATE TRIGGER del_cascade_del
BEFORE DELETE ON parent
FOR EACH ROW
WHEN (OLD.state <> "passive") -- only if not already passive
EXECUTE PROCEDURE trg_delbef();

mutating table when writing a trigger

I have been bogged in this problem for quite a long time....Can anyone help me out?
Here is the thing I want to implement:
I have a table A, A has attributes: id, count, total. Here I am required to implement such a trigger: IF the count in table A is updated, the trigger will set the total to 1.
My initial code is like this:
CREATE OR REPLACE TRIGGER tri_A AFTER UPDATE OF count ON A
FOR EACH ROW
BEGIN
UPDATE A SET total = 1 WHERE id = :new.id;
END;
/
The problem with this is the mutating table. When the table is updated, the table will be locked. I searched for the answers, I tried pragma autonomous_transaction, but I got an invalid trigger specification error. And there are other comments saying that we should try to use a combination of triggers to do this....I can't figure out how to do this
Assuming id is the primary key, you don't want to UPDATE the table. You just want to set :new.total. You'll also need to do this in a BEFORE UPDATE trigger not an AFTER UPDATE trigger.
CREATE OR REPLACE TRIGGER tri_A
BEFORE UPDATE OF count ON A
FOR EACH ROW
BEGIN
:new.total := 1;
END;

Create a trigger that updates a column on one table when a column in another table is updated

i have two tables
Order(id, date, note)
and
Delivery(Id, Note, Date)
I want to create a trigger that updates the date in Delivery when the date is updated in Order.
I was thinking to do something like
CREATE OR REPLACE TRIGGER your_trigger_name
BEFORE UPDATE
ON Order
DECLARE
BEGIN
UPDATE Delivery set date = ??? where id = ???
END;
How do I get the date and row id?
thanks
How do i get the date and row id?
Assuming these are columns on your ORDER table called DELIVERY_DATE and ID your trigger should look something like this:
CREATE OR REPLACE TRIGGER your_trigger_name
BEFORE UPDATE ON Order
FOR EACH ROW
BEGIN
if :new.delivery_date != :old.delivery_date
then
UPDATE Delivery d
set d.delivery_date = :new.delivery_date
where d.order_id = :new.id;
end if;
END;
Note the FOR EACH ROW clause: that is necessary to reference values from individual rows. I have used an IF construct to test whether to execute the UPDATE on Delivery. If you have no other logic in your trigger you could write it like this...
CREATE OR REPLACE TRIGGER your_trigger_name
BEFORE UPDATE OF delivery_date ON Order
FOR EACH ROW
BEGIN
UPDATE Delivery d
set d.delivery_date = :new.delivery_date
where d.order_id = :new.id;
END;
I have answered the question you asked but, as an aside, I will point out that your data model is sub-optimal. A properly normalized design would hold DELIVERY_DATE on only one table: DELIVERY seems teh logical place for it.
Use the OLD and NEW bind variables. OLD references the row or column being updated before the change is made; NEW references it after the change.
CREATE OR REPLACE TRIGGER trig1
BEFORE UPDATE
ON order REFERENCING NEW AS new
FOR EACH ROW
BEGIN
UPDATE delivery
SET ddate = :new.ddate
WHERE id = :new.id;
END;
You can modify the REFERENCING clause to give your bind variables different names. You can include OLD as <name> too. Example:
CREATE OR REPLACE TRIGGER trig1
BEFORE UPDATE
ON order REFERENCING OLD AS old_values NEW AS new_values
...
If you don't want to change the default names of "old" and "new", you can leave out the REFERENCING clause completely.
There is an implicit new and old reference in the trigger in the form of:
REFERENCING OLD AS OLD NEW AS NEW
You can write to the :NEW value but not to the :OLD value.
UPDATE Delivery set date = :new.delivery_date where id = :new.id;
CREATE OR REPLACE TRIGGER "BUR_TABLENAME" BEFORE
UPDATE ON "TABLE" FOR EACH ROW
BEGIN
If :new.active_date is not null Then
:new.active_date := TRUNC(:new.active_date);
End If;
END;
Template:
CREATE OR REPLACE TRIGGER TRIGGER_NAME
BEFORE
UPDATE
ON TABLE_NAME
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
V_VARIABLE NUMBER (1);
BEGIN
//Do Stuff;
null;
end;
Whenever there is a need for this kind of trigger, have a good look at your design. Is there really a need for a separate delivery record? Does an order really have more than 1 delivery ?
Triggers seem nice but they do tend to mess things up pretty quickly.