How to delete/update records inside trigger based on the updated/deleted row? - sql

I have a table with the following format
id | name | supervisor_id
I made a "BEFORE INSERT" trigger that checks if the supervisor_id exists in the id column and if not, then assign a null value to the supervisor_id.
I am trying to write two more triggers. One that checks if the supervisor_id exists in the id column before each update of the supervisor_id, and one that sets the supervisor_id to NULL for each employee if his supervisor is deleted.
This is my code, of course it's not working, help please.
CREATE OR REPLACE TRIGGER EAP_users_TRG3
AFTER DELETE
ON EAP_users
FOR EACH ROW
DECLARE
d NUMBER;
BEGIN
SELECT id INTO d FROM EAP_users WHERE id = :OLD.id;
UPDATE EAP_users SET supervisor = NULL WHERE supervisor = d;
END;
/
This is the "working" trigger:
CREATE OR REPLACE TRIGGER EAP_users_TRG1
BEFORE INSERT
ON EAP_users
FOR EACH ROW
DECLARE
supervisor EAP_users.supervisor%TYPE;
CURSOR supervisor_CUR IS SELECT idFROM EAP_users;
b BOOLEAN := FALSE;
BEGIN
IF ( :NEW.supervisor IS NOT NULL ) THEN
FOR s IN supervisor_CUR LOOP
IF ( :NEW.supervisor = s.id ) THEN
b := TRUE;
END IF;
END LOOP;
IF (b = FALSE) THEN
:NEW.supervisor := NULL;
END IF;
END IF;
END;
/

According to the definition of your problem, you are trying to enforce referential integrity of your data. In that case, a trigger is probably not the right tool. To quote Oracle's documentation:
You can use both triggers and integrity constraints to define and enforce any type of integrity rule. However, Oracle strongly recommends that you use triggers to constrain data input only in the following situations:
[...]
When a required referential integrity rule cannot be enforced using the following integrity constraints:
NOT NULL, UNIQUE
PRIMARY KEY
FOREIGN KEY
CHECK
DELETE CASCADE
DELETE SET NULL
In that particular case you should use FOREIGN KEY constraint using the DELETE SET NULL modifier. Assuming you have an index on id, all you need is:
ALTER TABLE EAP_users
ADD CONSTRAINT EAP_users_supervisor_cst
FOREIGN KEY (supervisor_id)
REFERENCES EAP_users(id)
ON DELETE SET NULL;
This simple referential integrity constraint will perform probably better the same things as your 3 triggers -- namely:
prevent insert/update with a non existing (non-NULL) supervisor_id
set all supervisor_id to NULL when you delete the supervisor
See http://sqlfiddle.com/#!4/1f8fb/1 for a live example.

Related

PostgreSQL trigger function not working properly

I have a trigger function:
CREATE OR REPLACE FUNCTION Day_21_bankTriggerFunction()
RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
DECLARE
act VARCHAR(30);
BEGIN
SELECT account_number INTO act
DELETE FROM depositor
WHERE depositor.account_number = act;
RETURN act;
END;
$$;
\`
and then I have a trigger:
CREATE TRIGGER Day_21_bankTrigger AFTER DELETE on account FOR EACH ROW EXECUTE PROCEDURE Day_21_bankTriggerFunction()
The thought behind this is that is an account was deleted from the account table then this should trigger the function to run and then delete all records on the depositor table where that account is present.
I can create the trigger function and trigger without an issues but if I attempt to delete an account from the account table...I still see the account # in the depositor table when I shouldn't.
Any thoughts?
above is what I tried. Expected results would be to delete an account from the account table and then the trigger function should kick off and remove that account from the depositor table
Usually, relationships of any mode between tables are created by foreign keys. This is the best way and are standards for DBs. Using foreign keys you can control your data. SQL sample:
CREATE TABLE contacts(
contact_id INT GENERATED ALWAYS AS IDENTITY,
customer_id INT,
contact_name VARCHAR(255) NOT NULL,
email VARCHAR(100),
PRIMARY KEY(contact_id),
CONSTRAINT fk_customer
FOREIGN KEY(customer_id)
REFERENCES customers(customer_id)
ON DELETE SET NULL
ON UPDATE SET NULL
);
Third, specify the parent table and parent key columns referenced by the foreign key columns in the REFERENCES clause.
Finally, specify the delete and update actions in the ON DELETE and ON UPDATE clauses.
The delete and update actions determine the behaviors when the primary key in the parent table is deleted and updated. Since the primary key is rarely updated, the ON UPDATE action is not often used in practice. We’ll focus on the ON DELETE action.
PostgreSQL supports the following actions after updating or deleting:
SET NULL (Set values to NULL if data exists on referencing table)
SET DEFAULT (Set values to DEFAULT VALUES of this field if data exists on referencing table)
RESTRICT (Similar to NO ACTION)
NO ACTION (Can not update or delete data if exists on referencing table)
CASCADE (Delete all data if exists on referencing table)
I wrote for you a sample trigger function:
CREATE OR REPLACE FUNCTION Day_21_bankTriggerFunction()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
declare
act varchar(30);
begin
-- get account_number from deleted record
act = old.account_number;
-- SECTION-1 :: Protect deleting if existing data
if (exists(select 1 from depositor where account_number = act)) then
return null;
end if;
-- SECTION-1 :: END
-- SECTION-2 :: Delete all data in the anothers table if exists */
delete from depositor where account_number = act;
return old;
-- SECTION-2 :: END
end
$function$;
CREATE TRIGGER Day_21_bankTrigger
BEFORE DELETE on account
FOR EACH ROW EXECUTE PROCEDURE Day_21_bankTriggerFunction();
Inside my trigger function, I have written two types of SQL codes. (SECTION-1, SECTION-2). You must choose one of them.

ORA-04091: table name is mutating - when trigger from child table wants to update parent table

I have 2 simple tables:
CREATE TABLE ORDERS
( ORDER_KEY number(10) NOT NULL,
ORDER_NR varchar2(50) NOT NULL,
LAST_UPDATE DATE,
CONSTRAINT ORDERS_PK PRIMARY KEY (ORDER_KEY)
);
CREATE TABLE ORDER_POSITIONS
( ORDER_POS_KEY number(10) NOT NULL,
ORDER_POS_NR number(10),
ORDER_POS_DESCRIPTION varchar2(50),
ORDER_KEY NUMBER(10) NOT NULL,
CONSTRAINT ORDER_POSITIONS_PK PRIMARY KEY (ORDER_POS_KEY),
CONSTRAINT ORDERS_FK
FOREIGN KEY (ORDER_KEY)
REFERENCES ORDERS(ORDER_KEY)
ON DELETE CASCADE
);
On the table ORDER_POSITIONS I created a trigger which should update the column LAST_UPDATE whenever a position is deleted.
CREATE OR REPLACE TRIGGER TGAUDIT_ORDER_POS
AFTER DELETE
ON ORDER_POSITIONS
FOR EACH ROW
DECLARE
BEGIN
UPDATE ORDERS O SET O.LAST_UPDATE = SYSDATE WHERE O.ORDER_KEY = :OLD.ORDER_KEY;
END;
If I delete a position from the table ORDER_POSITION, everything is working fine (the column LAST_UPDATE is updated).
However, if I want to delete an order, all its positions are gonna be deleted, too (via CASCADE DELETE). At this moment also the trigger on the table ORDER_POSITIONS is being raised and it wants to update the column of the table which is currently being deleted - ORDERS. Obviously I get here the error : ORA-04091 Table ORDERS is mutating.
Is there a way to get it solved?
I solved it finally using a compound trigger:
CREATE OR REPLACE TRIGGER TGAUDIT_ORDER_POS
FOR DELETE ON ORDER_POSITIONS
COMPOUND TRIGGER
TYPE parent_key_type IS TABLE OF ORDERS.ORDER_KEY%TYPE;
parent_keys parent_key_type := parent_key_type();
AFTER EACH ROW IS BEGIN
IF DELETING THEN
BEGIN
parent_keys.extend;
parent_keys(parent_keys.last) := :old.ORDER_KEY;
END;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT IS BEGIN
FOR i IN 1..parent_keys.count LOOP
UPDATE DEVART_TEST.ORDERS O SET O.LAST_UPDATE = SYSDATE WHERE O.ORDER_KEY = parent_keys(i);
END LOOP;
END AFTER STATEMENT;
END;
UPDATE: Another option would be to catch this specific exception within the regular trigger.
CREATE OR REPLACE TRIGGER TGAUDIT_ORDER_POS
AFTER DELETE
ON ORDER_POSITIONS
FOR EACH ROW
DECLARE
TABLE_MUTATING EXCEPTION;
PRAGMA EXCEPTION_INIT(TABLE_MUTATING, -4091 );
BEGIN
UPDATE ORDERS O SET O.LAST_UPDATE = SYSDATE WHERE O.ORDER_KEY = :OLD.ORDER_KEY;
EXCEPTION
WHEN TABLE_MUTATING THEN
NULL; -- suppress
END;
I would recreate the foreign-key constraint without the ON DELETE CASCADE clause and delete all order positions before deleting an order. That way you avoid the mutating table error by not having two tables mutating at the same time.

How to create a conditional trigger

I have a table with an id as auto incremented primary key and another id.
CREATE TABLE tester (
"id" integer PRIMARY KEY AUTOINCREMENT,
"refId" integer DEFAULT 0
);
refId should be able to either be 0 (the default) or reference id if refId > 0 (i.e. act as foreign key).
Now I need two constraints:
A row should only be deletable if its id is not used (referenced?) by any other row's refId
A row should only be deletable if its refId is 0.
From what I have understood, I need to create a trigger that checks for these constraints before a DELETE event happens. And depending on refId's value either abort the delete action or allow it.
However, I have a hard time understanding the syntax for this and how to do a conditional check. But what I have so far (in mind!) is concerning 1.):
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE "refId" = OLD."id") IS NOT NULL;
END;
And concerning 2.)
CREATE TRIGGER no_delete_if_ref
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
IF OLD."refId" > 0 THEN RAISE(ABORT, "cannot delete tester because it refers to an existing tester");
END;
Does this make sense and is valid?
I am totally not sure, to me it does but well, I am all noob.
Also as a last question, can I alternatively combine this into a single trigger? For example would this be a valid query:
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE ("refId" = OLD."id" OR "refId" > 0) ) IS NOT NULL;
END;
You can define a foreign key referring to the same table. Use null instead of 0 for rows without a reference:
create table tester(
id int primary key,
refid int references tester,
check (id <> refid)
);
insert into tester values
(1, null),
(2, null),
(3, 1),
(4, 3);
You need a trigger to ensure that a row which references another one cannot be deleted.
create or replace function before_delete_on_tester()
returns trigger language plpgsql as $$
begin
if old.refid is not null then
raise exception
'Cannot delete: (id)=(%) references (id)=(%)', old.id, old.refid;
end if;
return old;
end $$;
create trigger before_delete_on_tester
before delete on tester
for row execute procedure before_delete_on_tester();
Test:
delete from tester where id = 1;
ERROR: update or delete on table "tester" violates foreign key constraint "tester_refid_fkey" on table "tester"
DETAIL: Key (id)=(1) is still referenced from table "tester".
delete from tester where id = 4;
ERROR: Cannot delete from tester. (id)=(4) references (id)=(3)
CONTEXT: PL/pgSQL function before_delete_on_tester() line 4 at RAISE
In Postgres you have to define a trigger function. Read more:
Overview of Trigger Behavior
Trigger Procedures
Create Trigger

Use trigger to determine to implement insert or not

I am a beginner of SQL and Oracle database, and I need a little help about trigger. Here is the question:
I need to create a trigger that before insert a row into table Room, it will check this new row's hotel_id to see if it exists in another table Hotel. If the new hotel_id exists, then do the insert; if not, cancel this insert.
My code:
CREATE OR REPLACE TRIGGER TRIGGER1
BEFORE INSERT ON ROOM
FOR EACH ROW
BEGIN
if (:new.hotel_id in (select hotel_id from hotel)) then
--execute the insert;
else
--cancel the insert;
end if;
END;
I'm not sure that SQL has syntax that can be used to continue or cancel an operation. If there is, please teach me or attach the link related to it.
Correct way of doing this is using foreign key constraints.
You can define/alter your room table to refer it in the hotel_id column.
CREATE TABLE:
create table room (
. . .,
hotel_id int not null,
constraint fk_hotel_id foreign key (hotel_id)
references hotel(hotel_id)
);
ALTER TABLE:
alter table room
add constraint fk_hotel_id foreign key (hotel_id)
references hotel(hotel_id);
If the two table exists in different databases, then you can use trigger.
You can use raise_application_error proc to abort the execution and throw error.
create or replace trigger trigger1
before insert or update
on room for each row
declare
n integer := 0;
begin
select count(*) into n
from hotel
where hotel_id = :new.hotel_id;
if n = 0 then
raise_application_error(-20001, 'Hotel ID doesn't exist');
end if;
end;
As GurV said, foreign keys are more appropriate way for doing this
Though, here is trigger way:
CREATE OR REPLACE TRIGGER TRIGGER1
BEFORE INSERT ON ROOM
FOR EACH ROW
declare myvar INT;
BEGIN
SELECT 1 INTO myvar FROM Hotel WHERE hotel_id = :NEW.hotel_id FETCH NEXT 1 ROW ONLY;
exception
when no_data_found then
RAISE_APPLICATION_ERROR (-20000, 'some_error_message');
END;

How to maintain cross-table consistency with triggers in PostgreSQL?

Some interdependencies between database tables cannot be (easily) modeled with only foreign keys and check constraints. In my current project, I've begun writing constraint triggers for all these conditions, but it looks like I'm going to end up with hundreds of triggers if I go this route.
My main questions are:
Do the triggers and constraints in the scenario outlined below actually cover all the bases, or is it still possible to add/modify data in such a way that the result would be inconsistent?
Is writing all those triggers really the right way to go? I rarely see constraint triggers in 3rd-party DB schemas. Do other people just trust the application not to mess up?
Minimal example scenario
A central "inventory" table contains all tracked items. Some inventory items are of a specific type with specialized dimensions; these extra dimensions are stored in separate tables ("books", "pictures"). This basic table layout cannot be changed (this is just an example; the actual DB obviously has a lot more tables and columns).
Extra requirements:
(A) Every row in the "inventory" table whose type is "book" must have a matching row in "books" (same goes for "pictures")
(B) Every row in the "books" table must point to a unique row in "inventory" whose type is "book" (same goes for "pictures")
(C) Once inserted, an "inventory" record can never change its type
Complete database contents:
"inventory": id | type | name
----+------+----------------------
a | pic | panda.jpg
b | book | How to do stuff
c | misc | ball of wool
d | book | The life of π
e | pic | Self portrait (1889)
"pictures": inv_id | quality
--------+----------------------------
a | b/w photo?
e | nice, but missing right ear
"books": inv_id | author
--------+--------
b | Hiro P
d | Yann M
Create and populate the schema:
CREATE TABLE inventory (
id CHAR(1) PRIMARY KEY,
type TEXT NOT NULL CHECK (type IN ('pic', 'book', 'misc')),
name TEXT NOT NULL
);
CREATE TABLE pictures (
inv_id CHAR(1) PRIMARY KEY REFERENCES inventory(id) ON UPDATE CASCADE ON DELETE CASCADE,
quality TEXT
);
CREATE TABLE books (
inv_id CHAR(1) PRIMARY KEY REFERENCES inventory(id) ON UPDATE CASCADE ON DELETE CASCADE,
author TEXT
);
INSERT INTO inventory VALUES
('a', 'pic', 'panda.jpg'),
('b', 'book', 'How to do stuff'),
('c', 'misc', 'ball of wool'),
('d', 'book', 'The life of π'),
('e', 'pic', 'Self portrait (1889)');
INSERT INTO pictures VALUES
('a', 'b/w photo?'),
('e', 'nice, but missing right ear');
INSERT INTO books VALUES
('b', 'Hiro P'),
('d', 'Yann M');
Add triggers to maintain cross-table consistency:
-- TRIGGER: if inventory.type is 'book', there must be a corresponding record in
-- "books" (provides A, 1/2)
CREATE FUNCTION trg_inventory_insert_check_details () RETURNS TRIGGER AS $fun$
DECLARE
type_table_map HSTORE := hstore(ARRAY[
['book', 'books'],
['pic', 'pictures'] -- etc...
]);
details_table TEXT;
num_details INT;
BEGIN
IF type_table_map ? NEW.type THEN
details_table := type_table_map->(NEW.type);
EXECUTE 'SELECT count(*) FROM ' || details_table::REGCLASS || ' WHERE inv_id = $1'
INTO num_details
USING NEW.id;
IF num_details != 1 THEN
RAISE EXCEPTION 'A new "%"-type inventory record also needs a record in "%".',
NEW.type, details_table;
END IF;
END IF;
RETURN NULL;
END;
$fun$ LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER insert_may_require_details
AFTER INSERT ON inventory
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inventory_insert_check_details();
-- TRIGGER: when deleting details, parent must be gone, too (provides A, 2/2)
CREATE FUNCTION trg_inv_details_delete () RETURNS TRIGGER AS $fun$
BEGIN
IF EXISTS(SELECT 1 FROM inventory WHERE id = OLD.inv_id) THEN
RAISE EXCEPTION 'Cannot delete "%" record without deleting inventory record (id=%).',
TG_TABLE_NAME, OLD.inv_id;
END IF;
RETURN NULL;
END;
$fun$ LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER delete_parent_too
AFTER DELETE ON books
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inv_details_delete();
CREATE CONSTRAINT TRIGGER delete_parent_too
AFTER DELETE ON pictures
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inv_details_delete();
-- TRIGGER: details records must point to the correct inventory type (provides B)
CREATE FUNCTION trg_inv_details_check_parent_type () RETURNS TRIGGER AS $fun$
DECLARE
table_type_map HSTORE := hstore(ARRAY[
['books', 'book'],
['pictures', 'pic'] -- etc...
]);
required_type TEXT;
p_type TEXT;
BEGIN
required_type := table_type_map->(TG_TABLE_NAME);
SELECT type INTO p_type FROM inventory WHERE id = NEW.inv_id;
IF p_type != required_type THEN
RAISE EXCEPTION '%.inv_id (%) must point to an inventory item with type="%".',
TG_TABLE_NAME, NEW.inv_id, required_type;
END IF;
RETURN NULL;
END;
$fun$ LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER check_parent_inv_type
AFTER INSERT OR UPDATE ON books
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inv_details_check_parent_type();
CREATE CONSTRAINT TRIGGER check_parent_inv_type
AFTER INSERT OR UPDATE ON pictures
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inv_details_check_parent_type();
-- TRIGGER: value of inventory.type cannot be changed (provides C)
CREATE FUNCTION trg_fixed_cols () RETURNS TRIGGER AS $fun$
DECLARE
old_rec HSTORE := hstore(OLD);
new_rec HSTORE := hstore(NEW);
col TEXT;
BEGIN
FOREACH col IN ARRAY TG_ARGV LOOP
IF NOT (old_rec ? col) THEN
RAISE EXCEPTION 'Column "%.%" does not exist.', TG_TABLE_NAME, col;
ELSIF (old_rec->col) != (new_rec->col) THEN
RAISE EXCEPTION 'Column "%.%" cannot be modified.', TG_TABLE_NAME, col;
END IF;
END LOOP;
RETURN NULL;
END;
$fun$ LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER fixed_cols
AFTER UPDATE ON inventory
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_fixed_cols('type');
Zilk, the problem here is that you start out by saying that the schema cannot be modified. The reason you are needing to write this trigger spaghetti code is because the business logic and the design of your schema do not agree. For example, the trg_inv_details_delete is screaming at me as just an attempt to reinvent foreign key referential integrity—something that a DBMS like Postgres does for you.
This is actually a pretty typical subclass/superclass hierarchy problem that needs to be addressed in the design phase of the database. The details of the actual miniworld you are trying to represent is going to dictate how you would model this. From that knowledge would come an enhanced ERD, and then you would translate this conceptual model into a logical schema.
In this specific example, the superclass is Inventory and the subclasses are Books and Pictures. The subclasses form a partial/incomplete disjoint union with the superclass. I would go through the following references as this is a somewhat complex topic to describe here. Essentially, careful design and use of composite primary keys that base their foreign key on the superclass's key will take care of (B), and the presence of the Type attribute in Inventory would be unnecessary, thereby taking care of (A).
So your schema definition only needs a small change, which is to remove the Type attribute as it's redundant and would require triggers to keep the data aligned:
CREATE TABLE inventory (
id CHAR(1) PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE pictures (
inv_id CHAR(1) PRIMARY KEY REFERENCES inventory(id) ON UPDATE CASCADE ON DELETE CASCADE,
quality TEXT
);
CREATE TABLE books (
inv_id CHAR(1) PRIMARY KEY REFERENCES inventory(id) ON UPDATE CASCADE ON DELETE CASCADE,
author TEXT
);
Now, by nature of the schema design, there is no way to have an inventory item be considered a "book," for example, and yet not have the book attributes, so there is no need to have a trigger for that.
If you want to select all pictures and their picture-specific attributes:
SELECT id, name, quality
FROM inventory, pictures
WHERE id = inv_id;
If you want to select all pictures or misc items:
SELECT id, name
FROM inventory
WHERE id NOT IN (SELECT inv_id FROM books);
As far as requirement (C), never allowing an update on an attribute, that's kind of a strange one. This may actually be best done using permissions or something at the application level, as I can't see a scenario where you would want to make it impossible to fix a mistake.
See more on this:
Good Blog Post
Elmasri Chapter 8 Slides
Elmasri Chapter 9 Slides- Start reading on slide 17