backs up the flight ID, ticket number and seat number for any deleted ticket if the ticket corresponds to a flight that has not yet departed.
CREATE SEQUENCE generateKey
START WITH 100
INCREMENT BY 1;
CREATE TABLE backUpTicket
(
cancelledKey NUMBER NOT NULL,
flightID char(9) NOT NULL,
ticketNum varchar2(5) NOT NULL,
seatNum NUMBER(3) NOT NULL,
PRIMARY KEY(cancelledKey)
);
CREATE OR REPLACE TRIGGER backUpTicketCancelled
BEFORE DELETE ON FLIGHT
FOR EACH ROW
BEGIN
INSERT INTO backUpTicket
VALUES
(generateKey.nextVal, :old.flightID, :old.TicketNum, :old.seatNum);
IF(:old.actDepartDateTime IS NULL) THEN
DBMS_OUTPUT.PUT_LINE('The Flight has not departed yet');
END IF;
END backUpTicket;
/
the error coming in the IF(:old.actDepartDateTime IS NULL) as bad bind operator
First, your trigger should be on the TICKET table, not the FLIGHT table. Second, you're not supposed to create the backup of the ticket if the flight has already departed. Your code always creates the backup. I suggest the following might work better:
CREATE OR REPLACE TRIGGER backUpTicketCancelled
BEFORE DELETE ON TICKET
FOR EACH ROW
DECLARE
rowFlight FLIGHT%ROWTYPE;
BEGIN
SELECT *
INTO rowFlight
FROM FLIGHT f
WHERE f.FLIGHTID = :OLD.FLIGHTID;
IF rowFlight.actDepartDateTime IS NULL THEN
INSERT INTO backUpTicket
(CANCELLEDKEY, FLIGHTID, TICKETNUM, SEATNUM)
VALUES
(generateKey.nextVal, :old.flightID, :old.TicketNum, :old.seatNum);
ELSE
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SYSDATE', 'DD-MON-YYYY HH24:MI:SS') || ': ' ||
'Flight ' || :OLD.FLIGHTID || ' departed at ' ||
TO_CHAR(rowFlight.actDepartDateTime, 'DD-MON-YYYY HH24:MI:SS' ||
' so ticket #' || :OLD.TICKETNUM || ' for seat #' || :OLD.SEATNUM ||
' is not eligible for backup');
END IF;
END backUpTicket;
Also, the name of the backup table is supposed to be BACKUP_TICKET not backUpTicket, so you'll probably get marked down for that.
In addition, while I realize this is a homework assignment, this is a very poor design choice. You should create a procedure which performs all actions required to delete a ticket, rather than having "magic code" buried in a trigger which performs this kind of business decision.
Best of luck.
From your comments, your trigger is on table TICKET but the column actDepartDateTime is in table FLIGHT. You can only access columns from the trigger's table using :OLD and :NEW. To get actDepartDateTime you will need to select from the FLIGHT table using the :OLD.flightID value.
Related
I'm trying to create a trigger for updating some columns of a record after creating or updating the record.
There my code:
CREATE OR REPLACE TRIGGER "TRIGGER_UPDATE_CONTRACTOR_LOT"
AFTER INSERT OR UPDATE ON "CONTRACTOR_LOT"
FOR EACH ROW
DECLARE
CONTRACT_LOT_LABEL VARCHAR2(255 BYTE);
CONTRACTOR_LABEL VARCHAR2(200 BYTE);
BEGIN
SELECT LABEL INTO CONTRACT_LOT_LABEL FROM LOT_T WHERE ID = :NEW.LOT_ID;
SELECT CONTRACTOR INTO CONTRACTOR_LABEL FROM CONTRACTOR_T WHERE ID = :NEW.CONTRACTOR_ID;
UPDATE CONTRACTOR_LOT
SET LABEL = CONTRACT_LOT_LABEL || ':' || CONTRACTOR_LABEL,
FRAMEWORK_CONTRACT_NUMBER_LABEL = :NEW.ORDER || ':' || CONTRACT_LOT_LABEL || :NEW.FRAMEWORK_CONTRACT_NUMBER
WHERE ID = :NEW.ID;
END;
I get an error ORA-04091 (mutation)
I tried to add PRAGMA AUTONOMOUS_TRANSACTION; and I get the error ORA-00060 (deadlock detected while waiting for resource)
So I add COMMIT; after the update, but it's still the same issue.
Could you help me please with that?
You can't perform DML on the parent table within a trigger, and there's really rarely any reason to do so. Try this, with a BEFORE trigger and just modifying the NEW values of the existing INSERT or UPDATE that is firing the trigger:
CREATE OR REPLACE TRIGGER TRIGGER_UPDATE_CONTRACTOR_LOT
BEFORE INSERT OR UPDATE ON CONTRACTOR_LOT
FOR EACH ROW
DECLARE
CONTRACT_LOT_LABEL VARCHAR2(255 BYTE);
CONTRACTOR_LABEL VARCHAR2(200 BYTE);
BEGIN
SELECT LABEL INTO CONTRACT_LOT_LABEL FROM LOT_T WHERE ID = :NEW.LOT_ID;
SELECT CONTRACTOR INTO CONTRACTOR_LABEL FROM CONTRACTOR_T WHERE ID = :NEW.CONTRACTOR_ID;
:NEW.CONTRACTOR_LOT := CONTRACT_LOT_LABEL || ':' || CONTRACTOR_LABEL;
:NEW.FRAMEWORK_CONTRACT_NUMBER_LABEL := :NEW.ORDER || ':' || CONTRACT_LOT_LABEL || :NEW.FRAMEWORK_CONTRACT_NUMBER;
END;
Your trigger is catching changes and inserts on this table and then update on the same table. It seems to be vicious circle. Because update inside the trigger also will want to initiate this trigger
I am trying to implement a statement level trigger to enforce the following "An applicant cannot apply for more than two positions in one day".
I am able to enforce it using a row level trigger (as shown below) but I have no clue how to do so using a statement level trigger when I can't use :NEW or :OLD.
I know there are alternatives to using a trigger but I am revising for my exam that would have a similar question so I would appreciate any help.
CREATE TABLE APPLIES(
anumber NUMBER(6) NOT NULL, /* applicant number */
pnumber NUMBER(8) NOT NULL, /* position number */
appDate DATE NOT NULL, /* application date*/
CONSTRAINT APPLIES_pkey PRIMARY KEY(anumber, pnumber)
);
CREATE OR REPLACE TRIGGER app_trigger
BEFORE INSERT ON APPLIES
FOR EACH ROW
DECLARE
counter NUMBER;
BEGIN
SELECT COUNT(*) INTO counter
FROM APPLIES
WHERE anumber = :NEW.anumber
AND to_char(appDate, 'DD-MON-YYYY') = to_char(:NEW.appDate, 'DD-MON-YYYY');
IF counter = 2 THEN
RAISE_APPLICATION_ERROR(-20001, 'error msg');
END IF;
END;
You're correct that you don't have :OLD and :NEW values - so you need to check the entire table to see if the condition (let's not call it a "constraint", as that term has specific meaning in the sense of a relational database) has been violated:
CREATE OR REPLACE TRIGGER APPLIES_AIU
AFTER INSERT OR UPDATE ON APPLIES
BEGIN
FOR aRow IN (SELECT ANUMBER,
TRUNC(APPDATE) AS APPDATE,
COUNT(*) AS APPLICATION_COUNT
FROM APPLIES
GROUP BY ANUMBER, TRUNC(APPDATE)
HAVING COUNT(*) > 2)
LOOP
-- If we get to here it means we have at least one user who has applied
-- for more than two jobs in a single day.
RAISE_APPLICATION_ERROR(-20002, 'Applicant ' || aRow.ANUMBER ||
' applied for ' || aRow.APPLICATION_COUNT ||
' jobs on ' ||
TO_CHAR(aRow.APPDATE, 'DD-MON-YYYY'));
END LOOP;
END APPLIES_AIU;
It's a good idea to add an index to support this query so it will run efficiently:
CREATE INDEX APPLIES_BIU_INDEX
ON APPLIES(ANUMBER, TRUNC(APPDATE));
dbfiddle here
Best of luck.
Your rule involves more than one row at the same time. So you cannot use a FOR ROW LEVEL trigger: querying on APPLIES as you propose would hurl ORA-04091: table is mutating exception.
So, AFTER statement it is.
CREATE OR REPLACE TRIGGER app_trigger
AFTER INSERT OR UPDATE ON APPLIES
DECLARE
cursor c_cnt is
SELECT 1 INTO counter
FROM APPLIES
group by anumber, trunc(appDate) having count(*) > 2;
dummy number;
BEGIN
open c_cnt;
fetch c_cnt in dummy;
if c_cnt%found then
close c_cnt;
RAISE_APPLICATION_ERROR(-20001, 'error msg');
end if;
close c_cnt;
END;
Obviously, querying the whole table will be inefficient at scale. (One of the reasons why triggers are not recommended for this sort of thing). So this is a situation in which we might want to use a compound trigger (assuming we're on 11g or later).
I have 2 tables one is Schedule_table :
SID NOT NULL NUMBER(38)
FNUMBER VARCHAR2(20)
DEPARTURE_TIME TIMESTAMP(6) WITH TIME ZONE
ARRIVAL_TIME TIMESTAMP(6) WITH TIME ZONE
PRICE NUMBER
The second table is Flight_table
FNUMBER NOT NULL VARCHAR2(20)
DEPARTURE_APCODE CHAR(3)
ARRIVAL_APCODE CHAR(3)
Fnumber is PK in Flight_table and FK in Schedule_table.
I want the trigger to fire when the PRICE gets changed in Schedule_table
Also, to print a message of (Fnumber, ARRIVAL_APCODE, DEPARTURE_TIME ,ARRIVAL_APCODE).
I wrote this Code, it did not work.
create or replace trigger schedule_trigger after update on schedule
for each row when (new. price <> old. price)
begin
dbms_output.put_line( 'the flight number ' || :new.fnumber|| DEPARTURE_APCODE ||DEPARTURE_TIME|| ' has changed to '||:new.price ||' From'||:old.price);
end;
You are referring to the columns of Flight table - DEPARTURE_APCODE and DEPARTURE_TIME directly inside trigger which is not possible.You need to use a select
to fetch those values.
CREATE OR REPLACE TRIGGER schedule_trigger AFTER
UPDATE ON schedule
FOR EACH ROW
WHEN ( new.price <> old.price )
DECLARE
v_departure_apcode flight.departure_apcode%TYPE;
v_arrival_apcode flight.arrival_apcode%TYPE;
BEGIN
SELECT
departure_apcode,
arrival_apcode
INTO
v_departure_apcode,v_arrival_apcode
FROM
flight
WHERE
fnumber =:new.fnumber;
dbms_output.put_line('The flight number '
||:new.fnumber
|| v_departure_apcode
|| v_arrival_apcode
|| ' has changed to '
||:new.price
|| ' From '
||:old.price);
END;
/
And note that using DBMS_OUTPUT.PUT_LINE() inside a trigger is of no use as SQL developer sometimes does not display your output. Better create a log table and insert the records into it in your trigger.
You may require a commit statement for the output to be displayed.
SET SERVEROUTPUT ON;
UPDATE Schedule SET PRICE = 2502.00 where fnumber = 'F2004' ;
commit;
Commit complete.
The flight number F2004AB CD has changed to 2502 From 2501
As the table is mutating the following trigger does not work as I believe the SQL statement within the trigger cannot be executed against a mutating table, however as I am not on 11g I cannot create a compound trigger. I have tried including PRAGMA AUTONOMOUS TRANSACTION; in the declaration section, however this would not compile. Could anyone provide me with the best solution?
create or replace
trigger fb_pers_id_check2_tr
--after insert on ifsapp.person_info_tab
before insert on ifsapp.person_info_tab
for each row
begin
declare
-- pragma autonomous_transaction;
v_pid_ person_info_tab.person_id%type;
format_name_ person_info_tab.name%type;
begin
v_pid_ := :new.person_id;
select regexp_replace(upper(:new.name), '\W')
into format_name_
from ifsapp.person_info_tab
where person_id = v_pid_;
if length(v_pid_) < 3 and (length(format_name_) < 21 and v_pid_ <> format_name_) then
raise_application_error(-20001, 'Person ID: ' || v_pid_ || 'is not valid, please enter a valid Person ID, e.g. "' || format_name_ || '".');
end if;
end;
end fb_pers_id_check2_tr;
N.B. In plain English this trigger is intended to stop users setting a person id that is less than 3 characters long and does not equal variable 'format_name_' if it is less than 21 characters long.
Oracle doesn't allow a row trigger to read or modify the table on which the trigger is defined. However, if PERSON_ID is a PRIMARY or UNIQUE key on PERSON_INFO_TAB (which seems to be the case given that it's used in a singleton SELECT) you don't really need to read the table - just use the OLD or NEW values where appropriate:
create or replace trigger fb_pers_id_check2_tr
before insert on ifsapp.person_info_tab
for each row
declare
v_pid_ person_info_tab.person_id%type;
format_name_ person_info_tab.name%type;
begin
v_pid_ := :new.person_id;
format_name_ := REGEXP_REPLACE(UPPER(:new.name), '\W');
if length(v_pid_) < 3 and
length(format_name_) < 21 and
v_pid_ <> format_name_
then
raise_application_error(-20001, 'Person ID: ' || v_pid_ ||
' is not valid, please enter a valid' ||
' Person ID, e.g. "' || format_name_ || '".');
end if;
end fb_pers_id_check2_tr;
Here the code is checking the NEW value of NAME (which I think is right, given that it appears to be validating input), but if the intent was to check the OLD value it's simple to change :NEW to :OLD in the REGEXP_REPLACE call.
Share and enjoy.
I am just starting to learn triggers so please bear with me. If the row being inserted has a gift that is the same as any gift already in the table, print a message saying that the gift was already given to receiver from donor.
create or replace TRIGGER Same_Gift_Given
BEFORE INSERT ON GIVING
FOR EACH ROW
DECLARE
giftgiven varchar(255);
BEGIN
SELECT giftname INTO giftgiven from GIVING;
IF :new.giftname = giftgiven then
dbms_output.put_line(giftgiven || ' has already been gifted to ' || giving.receiver || ' by ' || giving.donor);
end if;
END;
This is a really awful homework problem. You would never, ever, ever us a trigger to do anything like this in a real system. It will break most INSERT operations and it will fail if there are ever multiple users. In reality, you would use a constraint. In reality, if for some reason you were forced at gunpoint to use a trigger, you would need a series of three triggers, a package, and a collection to do it properly.
What the professor is probably looking for
Just to emphasize, though, you would never, ever consider doing this in a real system
create or replace trigger same_gift_given
before insert on giving
for each row
declare
l_existing_row giving%rowtype;
begin
select *
into l_existing_row
from giving
where giftname = :new.giftname
and rownum = 1;
dbms_output.put_line( :new.giftname ||
' has already been gifted to ' ||
l_existing_row.receiver ||
' from ' ||
l_existing_row.donor );
exception
when no_data_found
then
null;
end;
This does not prevent you from inserting duplicate rows. It will throw a mutating trigger error if you try to do anything other than an INSERT ... VALUES on the giving table. It is inefficient. It does not handle multiple sessions. In short, it is absolutely atrocious code that should never be used in any real system.
What you would do in reality
In reality, you would create a constraint
ALTER TABLE giving
ADD CONSTRAINT unique_gift UNIQUE( giftname );
That will work in a multi-user environment. It will not throw a mutating trigger exception. It is much more efficient. It is much less code. It actually prevents duplicate rows from being inserted.
Let's try something a bit different:
CREATE OR REPLACE TRIGGER GIVING_COMPOUND_INSERT
FOR INSERT ON GIVING
COMPOUND TRIGGER
TYPE STRING_COL IS TABLE OF VARCHAR2(255) INDEX BY VARCHAR2(255);
colGiftnames STRING_COL;
aGiftname VARCHAR2(255);
nCount NUMBER;
-- Note that the way the associative array is used here is a bit of a cheat.
-- In the BEFORE EACH ROW block I'm putting the string of interest into the
-- collection as both the value *and* the index. Then, when iterating the
-- collection only the index is used - the value is never retrieved (but
-- since it's the same as the index, who cares?). I do this because I'd
-- rather not write code to call a constructor and maintain the collections
-- size - so I just use an associative array and let Oracle do the work for
-- me.
BEFORE EACH ROW IS
BEGIN
colGiftnames(:NEW.GIFTNAME) := :NEW.GIFTNAME;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
aGiftname := colGiftnames.FIRST;
WHILE aGiftname IS NOT NULL LOOP
SELECT COUNT(*)
INTO nCount
FROM GIVING
WHERE GIFTNAME = aGiftname;
IF nCount > 1 THEN
DBMS_OUTPUT.PUT_LINE('Found ' || nCount || ' instances of gift ''' ||
aGiftname || '''');
RAISE_APPLICATION_ERROR(-20001, 'Found ' || nCount ||
' instances of gift ''' ||
aGiftname || '''');
END IF;
aGiftname := colGiftnames.NEXT(aGiftname);
END LOOP;
END AFTER STATEMENT;
END GIVING_COMPOUND_INSERT;
Again, this is a LOUSY way to try to guarantee uniqueness. In practice the "right way" to do this is with a constraint (either UNIQUE or PRIMARY KEY). Just because you can do something doesn't mean you should.
Share and enjoy.