Use trigger to determine to implement insert or not - sql

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;

Related

PostgreSQL trigger update average and count when new value in column is added

This is my table:
CREATE TABLE mark (
EID serial,
PID integer,
SID integer,
score integer DEFAULT 5 NOT NULL,
CONSTRAINT PK_EID PRIMARY KEY(EID),
CONSTRAINT "FK_personne_ID"
FOREIGN KEY (PID)
REFERENCES personne (PID)
ON UPDATE RESTRICT ON DELETE RESTRICT,
CONSTRAINT "FK_serie_ID"
FOREIGN KEY (SID)
REFERENCES serie (SID)
ON UPDATE RESTRICT ON DELETE RESTRICT
);
My trigger : when a new row/value is inserted in the column "score", updates the average score and the total count of scores:
I'm not sure if I should implement a function ahead or start with the trigger directly :
CREATE OR REPLACE FUNCTION FunctionUpdateScore(float) RETURNS integer AS
'BEGIN
SELECT COUNT(score) AS nb_score, AVG(score) AS ag_score
FROM mark;
END;'
LANGUAGE 'plpgsql';
--trigger
CREATE or REPLACE TRIGGER TriggerUpdateScore
AFTER INSERT
ON mark
FOR EACH ROW
EXECUTE PROCEDURE FunctionUpdateScore();
UPDATE nb_score
SET nb_score= nb_score+ 1
END;
Properly escape your trigger body. Use dollar quoting
Remove the float argument from your function
Actually perform the update in your function
Update the correct table (you are updating the nb_score table in your example which does not have a trigger on it, therefore it will not fire off the trigger)
Specify a relationship between mark and nb_score. As of now the trigger function below updates all nb_score rows, not just the one that is related to the inserted row in mark.;
Your function return type must be trigger
CREATE OR REPLACE FUNCTION FunctionUpdateScore() RETURNS trigger AS $$
BEGIN
UPDATE nb_score
SET
nb_score=COUNT(score),
ag_score=AVG(score)
FROM mark;
END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER TriggerUpdateScore
AFTER INSERT ON mark FOR EACH ROW EXECUTE PROCEDURE FunctionUpdateScore();

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 avoid mutating table in a trigger when working with nested table, and do an update into another table?

I have a code that uses a trigger to do an update into a table after a dml insert has been done, but i need the information inside an atributte that is a nested table, and with that information do the update, but my code throws a mutating table error, and i want to know what is it that i am doing wrong, this is the code with the tables, types and trigger involved.
----create the type tipo_detalle---------------
CREATE OR REPLACE TYPE tipo_detalle AS OBJECT(
codigo NUMBER(1),
cantidad NUMBER(2)
);
/
-----declare the nested table with type tipo_detalle---
CREATE OR REPLACE TYPE detalle_anidado AS TABLE OF tipo_detalle;
/
---------create the table pedido------------------------
CREATE TABLE pedido(
cod_bodega REFERENCES bodega,
dia NUMBER(8),
columna_detalle detalle_anidado,
PRIMARY KEY(cod_bodega,dia)
)
NESTED TABLE columna_detalle STORE AS columna_detalle_anidada
((PRIMARY KEY(NESTED_TABLE_ID,codigo)));
---table where i am going to insert after insert in pedido----
CREATE TABLE inventario(
cod_bodega REFERENCES bodega,
cod_producto REFERENCES producto,
existencia NUMBER(8),
PRIMARY KEY(cod_bodega,cod_producto)
);
------ trigger to do an insert into inventario-----------
CREATE OR REPLACE TRIGGER triggers_de_pedido
FOR INSERT OR UPDATE OR DELETE ON pedido
COMPOUND TRIGGER
contador NUMBER(8);
fila pedido.columna_detalle%TYPE;
cod_producto_ NUMBER(1);
cantidad_ NUMBER(2);
indice NUMBER(4);
AFTER EACH ROW IS
BEGIN
IF INSERTING THEN
SELECT columna_detalle INTO fila FROM pedido WHERE
cod_bodega=:NEW.cod_bodega AND dia=:NEW.dia;
indice:=fila.FIRST;
WHILE indice IS NOT NULL LOOP
cod_producto_:=fila(indice).codigo;
cantidad_:=fila(indice).cantidad;
UPDATE inventario SET existencia=existencia-cantidad_
WHERE cod_bodega=:NEW.cod_bodega AND cod_producto=cod_producto_;
indice:=fila.NEXT(indice);
END LOOP;
END IF;
END AFTER EACH ROW;
END triggers_de_pedido;
/
The error is raised because you are selecting from the Trigger owner(the table pedido) inside the Trigger.
Change this select statement
SELECT columna_detalle INTO fila FROM pedido WHERE ...
to
IF INSERTING THEN
fila := :NEW.columna_detalle;

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

Insert trigger ends up inserting duplicate rows in partitioned table

I have a partitioned table with (what I think) the appropriate INSERT trigger and a few constraints on it. Somehow, INSERT statements insert 2 rows for each INSERT: one for the parent and one for the appropriate partition.
The setup briefly is the following:
CREATE TABLE foo (
id SERIAL NOT NULL,
d_id INTEGER NOT NULL,
label VARCHAR(4) NOT NULL);
CREATE TABLE foo_0 (CHECK (d_id % 2 = 0)) INHERITS (foo);
CREATE TABLE foo_1 (CHECK (d_id % 2 = 1)) INHERITS (foo);
ALTER TABLE ONLY foo ADD CONSTRAINT foo_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo ADD CONSTRAINT foo_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_d_id_key UNIQUE (d_id, label);
CREATE OR REPLACE FUNCTION foo_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.id IS NULL THEN
NEW.id := nextval('foo_id_seq');
END IF;
EXECUTE 'INSERT INTO foo_' || (NEW.d_id % 2) || ' SELECT $1.*' USING NEW;
RETURN NEW;
END
$$
LANGUAGE plpgsql;
CREATE TRIGGER insert_foo_trigger
BEFORE INSERT ON foo
FOR EACH ROW EXECUTE PROCEDURE foo_insert_trigger();
Upon further debugging I isolated what's causing it: the fact that the INSERT trigger returns NEW as opposed to just NULL. However I do want my insert statements to return the auto-increment id and if I just return NULL that won't be the case.
What's the solution? Why does returning NEW cause this seemingly "strange" behavior?
UPDATE #1
Well, I know why the rows got inserted twice as it is clear from the documentation of triggers:
Trigger functions invoked by per-statement triggers should always
return NULL. Trigger functions invoked by per-row triggers can return
a table row (a value of type HeapTuple) to the calling executor, if
they choose. A row-level trigger fired before an operation has the
following choices:
It can return NULL to skip the operation for the current row. This
instructs the executor to not perform the row-level operation that
invoked the trigger (the insertion, modification, or deletion of a
particular table row).
For row-level INSERT and UPDATE triggers only, the returned row
becomes the row that will be inserted or will replace the row being
updated. This allows the trigger function to modify the row being
inserted or updated.
But my question is still how is it possible to not return NEW and still be able to get the auto-incremented id, or ROW_COUNT for example?
UPDATE #2
I found a solution, but I sure hope that there's a better one. Basically, you can add an AFTER TRIGGER to delete the row inserted into the parent table. This seems horribly inefficient, so if anyone has a better solution, please post it!
For reference the solution is:
CREATE TRIGGER insert_foo_trigger
BEFORE INSERT ON foo
FOR EACH ROW EXECUTE PROCEDURE foo_insert_trigger();
CREATE OR REPLACE FUNCTION foo_delete_master()
RETURNS TRIGGER AS $$
BEGIN
DELETE FROM ONLY foo WHERE id = NEW.id;
RETURN NEW;
END
$$
LANGUAGE plpgsql;
CREATE TRIGGER after_insert_foo_trigger
AFTER INSERT ON foo
FOR EACH ROW EXECUTE PROCEDURE foo_delete_master();
A simpler way is to create stored procedure instead of the triggers, for example add_foo( [parameters] ), which would decide which partition is suitable to insert a row to and return id (or the new record values, including id). For example:
CREATE OR REPLACE FUNCTION add_foo(
_d_id INTEGER
, _label VARCHAR(4)
) RETURNS BIGINT AS $$
DECLARE
_rec foo%ROWTYPE;
BEGIN
_rec.id := nextval('foo_id_seq');
_rec.d_id := _d_id;
_rec.label := _label;
EXECUTE 'INSERT INTO foo_' || ( _d_id % 2 ) || ' SELECT $1.*' USING _rec;
RETURN _rec.id;
END $$ LANGUAGE plpgsql;
Another solution to this problem is offered by this question:
Postgres trigger-based insert redirection without breaking RETURNING
In summary, create a view for your table and then you can use INSTEAD OF to handle the update while still being able to return NEW.
Untested code, but you get the idea:
CREATE TABLE foo_base (
id SERIAL NOT NULL,
d_id INTEGER NOT NULL,
label VARCHAR(4) NOT NULL
);
CREATE OR REPLACE VIEW foo AS SELECT * FROM foo_base;
CREATE TABLE foo_0 (CHECK (d_id % 2 = 0)) INHERITS (foo_base);
CREATE TABLE foo_1 (CHECK (d_id % 2 = 1)) INHERITS (foo_base);
ALTER TABLE ONLY foo_base ADD CONSTRAINT foo_base_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_base ADD CONSTRAINT foo_base_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_d_id_key UNIQUE (d_id, label);
CREATE OR REPLACE FUNCTION foo_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.id IS NULL THEN
NEW.id := nextval('foo_base_id_seq');
END IF;
EXECUTE 'INSERT INTO foo_' || (NEW.d_id % 2) || ' SELECT $1.*' USING NEW;
RETURN NEW;
END
$$
LANGUAGE plpgsql;
CREATE TRIGGER insert_foo_trigger
INSTEAD OF INSERT ON foo
FOR EACH ROW EXECUTE PROCEDURE foo_insert_trigger();