Conditional CASCADE operation for foreign key constraint? - sql

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

Related

PostgreSQL Function/Trigger assign same ID for every row per call

I want to insert into a child table every time the parent table is updated. But when this happens, all of the new records inserted into the child table should have the same id. The ids will only increment if the parent table is separately updated another time. How can I do this?
In this case, I want to insert into the child table every new My_Date field from when the parent table is updated. Below is an example of what this would look like.
When parent table gains two new rows...
My_Date
old
old
new
new
Child table gains two new rows, both assigned to same ID (the ID autoincrements in table definition)
My_Date ID
...
new 4
new 4
When parent table gains two new rows again...
My_Date
old
old
old
old
new
new
Child table gains two new rows, both assigned to same new ID
My_Date ID
...
old 4
old 4
new 5
new 5
Here is what I have so far.
CREATE or replace FUNCTION update_child() RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO child
SET My_Date = NEW.My_Date /*Not sure if this is correct*/
/*Give every row the same ID*/
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_child_after_update
AFTER UPDATE
ON parent
FOR EACH ROW
EXECUTE PROCEDURE update_child();
I believe what you want is a combination of a sequence and a statement-level (as opposed to row level) trigger:
https://www.postgresql.org/docs/current/trigger-definition.html#:~:text=PostgreSQL%20offers%20both%20per%2Drow,statement%20that%20fired%20the%20trigger.
PostgreSQL offers both per-row triggers and per-statement triggers.
With a per-row trigger, the trigger function is invoked once for each
row that is affected by the statement that fired the trigger. In
contrast, a per-statement trigger is invoked only once when an
appropriate statement is executed, regardless of the number of rows
affected by that statement. In particular, a statement that affects
zero rows will still result in the execution of any applicable
per-statement triggers. These two types of triggers are sometimes
called row-level triggers and statement-level triggers, respectively.
This is really bare bones, but I think it will demonstrate the desired behavior you described in your question.
create sequence child_id;
CREATE or replace FUNCTION update_child()
RETURNS trigger AS
$BODY$
DECLARE
r1 record;
new_id int;
BEGIN
new_id := nextval ('child_id');
FOR r1 IN SELECT * FROM new_table
LOOP
insert into child
select r1.my_date, new_id;
END loop;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_child_after_update
AFTER UPDATE
ON parent
REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
FOR EACH statement
EXECUTE PROCEDURE update_child();
CREATE TRIGGER update_child_after_insert
AFTER INSERT
ON parent
REFERENCING NEW TABLE AS new_table
FOR EACH statement
EXECUTE PROCEDURE update_child();
There are a lot of restrictions on statement-level triggers, and it's worth reading up on them. For example, only the "after" is supported.
Presumably your parent table also has some form of PK, which you would also be passing to the child, but I'm hopeful that's easy enough to see where you would insert that in the code example above.

updating object inside BEFORE DELETE trigger has no effect

I need to clean up a deleted object within another object which uses it as a foreign key, and I use a BEFORE DELETE trigger for that. I have no idea why the code below does not work.
Even more strange is that if I perform sequentially the UPDATE query and then DELETE query, the row is deleted properly.
CREATE OR REPLACE FUNCTION cleanNoteEventConnections() RETURNS TRIGGER AS $$
DECLARE
BEGIN
EXECUTE 'update invoice set "noteEvent"=null where "noteEvent"=' || OLD.id;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER cleanNoteEventConnections BEFORE DELETE ON note_event
FOR EACH ROW EXECUTE PROCEDURE cleanNoteEventConnections();
This is what I see in the pgAdmin console after the delete query:
delete from note_event where id=34
result: Query returned successfully: 0 rows affected, 11 msec execution time.
And the note_event with id 34 still exists.
This behavior is described in the documentation:
Row-level triggers fired BEFORE can return null to signal the trigger manager to skip the rest of the operation for this row (i.e., subsequent triggers are not fired, and the INSERT/UPDATE/DELETE does not occur for this row). If a nonnull value is returned then the operation proceeds with that row value.
Use RETURN OLD; instead of RETURN NULL;.

postgresql triggers possible infinte loop?

I have a table with 3 fields
id, name , value
I want to add a 4th colum calcValue. The calculation of calcValue is based on the filed value in the whole table, changing it in one row can cause changes in all of the others.
I want to write a trigger that updates calcValue everytime there is insert, delete or update in the table.
What i'm worry about is that the trigger itself is going to have an Update command. Will it case another call to this trigger? Will I be stuck with infinte loop?
To describe it better:
CREATE TRIGGER x
AFTER INSERT OR UPDATE OR DELETE
ON a
FOR EACH ROW
EXECUTE PROCEDURE dosomething();
and:
CREATE OR REPLACE FUNCTION dosomething()
RETURNS trigger AS
$BODY$
begin
code + calculation of result...
for row in
QUERY
Loop
Update a set calValue=result where id=...
end loop;
end;
$BODY$
LANGUAGE plpgsql VOLATILE
Will the update of a in dosomething() will cause another invoke of the trigger x? If it does is there a way to handle it so it won't stuck in infinte loop?
My goal is to do dosomething() once per update/insert/delete action of a . I don't want dosomething() to be called again because of the update a in the trigger.
Edit: Since i'm updating in the trigger a diffrent column I can do that:
CREATE TRIGGER x
BEFORE UPDATE OF value ON a
FOR EACH ROW
EXECUTE PROCEDURE dosomething();
this should solve my problem as the trigger updates calcValue and the tigger isn't set invoke on value column. However I would still like to know if there is an answer to my original question... suppose that the trigger would have update the same column.
Another alternative would be to add a when clause, along the lines of:
CREATE TRIGGER x
AFTER INSERT OR UPDATE OR DELETE
ON a
FOR EACH ROW
WHEN NEW IS NULL OR OLD IS NULL OR NEW.value <> OLD.value
EXECUTE PROCEDURE dosomething();
It will only execute the trigger when there's a change in the value field. Therefore when inside the trigger you update calcValue, the trigger is not called again, preventing an infinite "loop".

Creating a Trigger To Delete Records

I have a table which we can call decks. Decks has an id used as its primary key along with some other attributes. The cards table contains a foreign key reference to the deck id and has a primary key of cardid as well. Another table exists called answers where its foreign key is the cardid.
So in order to delete from decks, the database requires I delete from answers first, then cards, and then finally from decks.
I would like to create a trigger which takes care of the first and second delete so that I only have to specify a delete statement from the decks table to completely destroy a deck.
Below is an example PostgreSQL trigger I've found, but I am not sure if its even possible to do what I am asking as I can find no examples online of anyone creating a trigger this way.
CREATE OR REPLACE FUNCTION autoCalculate() RETURNS TRIGGER AS $$
BEGIN
IF NEW.wins < 0 THEN
RAISE EXCEPTION 'Wins cannot be negative';
END IF;
IF(OLD.wins <> NEW.wins_ OR (OLD.losses <> NEW.losses) THEN
NEW.Winning_Percentage := calc_winning_percentage(NEW.Wins, NEW.Losses);
END IF
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER update_winning_percentage ON standings;
CREATE TRIGGER update_winning_percentage BEFORE INSERT OR UPDATE ON standings
FOR EACH ROW EXECUTE PROCEDURE autoCalculate();
If anyone has knowledge to do this, if they could nudge me in the right direction or provide an example of how to do this I'd be grateful!
On your foreign key definitions use ON DELETE CASCADE and it will take care of this for you
In Postgres 8.2 you can specify On Delete Casacde when you define your foreign key relationships.
http://www.postgresql.org/docs/8.2/static/ddl-constraints.html

Oracle trigger- instead of delete, update the row

How do I write an Oracle trigger, than when a user deletes a certain record, the delete doesnt actually happen, but instead performs an update on those rows and sets the status of the record to 'D'?
I tried:
create or replace
trigger DELFOUR.T4M_ITEM_ONDELETE
before delete on M_ITEM_H
FOR EACH ROW
BEGIN
UPDATE
M_ITEM_H
SET
ITEM_STAT = 'D'
WHERE
CUST_CODE = 'TEST'
AND ITEM_CODE = 'GDAY'
;
raise_application_error(-20000,'Cannot delete item');
END;
But I am getting mutating table errors. Is this possible?
If you really need a trigger, the more logical approach would be to create a view, create an INSEAD OF DELETE trigger on the view, and to force the applications to issue their deletes against the view rather than against the base table.
CREATE VIEW vw_m_item_h
AS
SELECT *
FROM m_item_h;
CREATE OR REPLACE TRIGGER t4m_item_ondelete
INSTEAD OF DELETE ON vw_m_item_h
FOR EACH ROW
AS
BEGIN
UPDATE m_item_h
SET item_stat = 'D'
WHERE <<primary key>> = :old.<<primary key>>;
END;
Better yet, you would dispense with the trigger, create a delete_item procedure that your application would call rather than issuing a DELETE and that procedure would simply update the row to set the item_stat column rather than deleting the row.
If you really, really, really want a solution that involves a trigger on the table itself, you could
Create a package with a member that is a collection of records that map to the data in the m_item_h table
Create a before delete statement-level trigger that empties this collection
Create a before delete row-level trigger that inserts the :old.<<primary key>> and all the other :old values into the collection
Create an after delete statement-level trigger that iterates through the collection, re-inserts the rows into the table, and sets the item_stat column.
This would involve more work than an instead of trigger since you'd have to delete and then re-insert the row and it would involve way more moving pieces so it would be much less elegant. But it would work.
First of all the trigger you wrote would throw a mutating table error. Technically what you are asking is not possible i.e. delete wouldn't delete but rather update, unless you raise an exception in the middle which could be an ugly way of doing it. I would think users using some sort of application front end which lets them delete data using a delete button, so you may use an update statement there instead of a delete statement.
Another option would be to create a log table, where you could insert the record before deleting it from the actual table and then join the log table with the actual table to retrieve deleted records. Something like-
CRETAE TABLE M_ITEM_H_DEL_LOG as SELECT * FROM M_ITEM_H WHERE 1=2;
And then
create or replace
trigger DELFOUR.T4M_ITEM_ONDELETE
before delete on M_ITEM_H
FOR EACH ROW
BEGIN
INSERT INTO
M_ITEM_H_DEL_LOG
VALUES (:old.col1, :old.col2,.....) --col1, col2...are columns in M_ITEM_H
;
END;