PL/SQL TRIGGER COMPILE ERROR - oracle

create or replace trigger enroll_bef_ins_row
before insert on enrollments
for each row
declare
original number;
seatsremain_already_full exception;
begin
Select seatsremain into original from offering where offerno= :new.offerno;
if original > 0 then
update offering set seatsremain= seatsremain - 1;
dmbs_output.put_line ('Seats available in offering' |offerno| 'have decreased from' |:old.seatsremain| to |:new.seatsremain|);
else if original = 0 then
dbms_output.put_line ('Offering' |offerno| 'is already full!');
raise seatsremain_already_full
end if;
exception
when seatsremain_already_full
raise_application_error (-20001, 'Cannot allow insertion');
end
/
I keep getting a "trigger created with compilation error" message and every time I try to insert in values I get a ORA 04098 - SYSTEM.ENROLL_BEFORE_INS_ROW IS INVALID AND FAILED REVALIDATION message.
My task is to write a trigger that performs the following tasks before a row has been inserted into the enrollments table:
If the seats are available for the particular offering, the trigger should automatically decrease the number of seats for the offering and display the message: seats availabe in offering (offering number) have decreased from (number of seats available prior to insertion) to (number of seats available after insertion). If the number of seats available after the insertion is equal to 0, display message: 'No more seats in offering (offering number should be entered here)
If the seats available for a particular offering were equal to 0 prior the insertion of the row in enrollments, the trigger should display the following:
insertion not allowed
and use the raise_application_error procedure to prevent the execution of the INSERT statement

The concatenation operator is ||, and all statements must end with a semi-colon. There was also the odd apostrophe missing here and there.
Try this:
create or replace trigger enroll_bef_ins_row
before insert on enrollments
for each row
declare
original number;
seatsremain_already_full exception;
begin
Select seatsremain
into original
from offering
where offerno = :new.offerno;
if original > 0 then
update offering
set seatsremain = seatsremain - 1
WHERE OFFERNO = :new.OFFERNO;
dmbs_output.put_line ('Seats available in offering' || :new.offerno ||
'have decreased from' || original ||
' to ' || original-1);
elsif original = 0 then
dbms_output.put_line ('Offering' || offerno ||
' is already full!');
raise seatsremain_already_full;
end if;
exception
when seatsremain_already_full
raise_application_error (-20001, 'Cannot allow insertion');
end;
I suggest that you might want to NOT decrement SEATSREMAIN but instead compute the number of seats remaining by taking OFFERING.SEATSAVAILABLE and substracting the sum of the seats taken - otherwise there's a race condition in there on OFFERING.SEATSREMAIN and you can end up overbooking - but this will probably work for simple cases. In addition, you might want to raise an error if SEATSREMAIN ends up negative.
Share and enjoy.

Related

Statement level trigger to enforce a constraint

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).

Oracle - Calling Stored Procedure in a Loop in another Stored Procedure

I reviewed the answers to similar inquiries but continue to get another error that cannot seem to resolved by adding in the schema the proc was created under.
This is the code I am attempting to run:
create or replace procedure prcReturnDVD
-- define the parameters that this proc will accept
(memberid_in integer, dvd_in integer)
is
-- Define local variables
vNumRem number(2);
BEGIN
-- Update the rental table with the current date
update rental set rentalreturneddate = current_date where memberid = memberid_in and dvdid = dvd_in;
-- update DVD quantityonhand to reflect the return
update dvd set dvdquantityonhand = dvdquantityonhand + 1 where dvdid = dvd_in;
-- Check to see how many DVDs are available for rent for this member
select get_rentalsremaining(memberid_in) into vNumRem from dual;
-- Evaluate the next action depending on number of remaining DVD rentals available
if vNumRem >= 1 then
-- need to write a for loop in order to cycle through the DVDs that need to be shipped out
while vNumRem >= 0
loop
SYSTEM.prcShipNextDVD(memberid_in);
vNumRem := vNumRem - 1;
end loop;
elsif vNumRem = 0 then
-- message that no rentals are allowed
dbms_output.put_line('No more rentals allowed.');
end if;
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('No Data Returned, process failed.');
WHEN TOO_MANY_ROWS THEN
dbms_output.put_line('Too many rows returned into variable, check data and try again.');
WHEN OTHERS THEN
dbms_output.put_line('An unidentified error has occured. Please research issue with procedure.');
END;
/
Inside the LOOP statement I am attempting to call another procedure written previous by the same user (I'm using SYSTEM only because it's only a homework assignment and it simplifies permissions):
create or replace procedure prcShipNextDVD
-- define the parameters that this proc will accept
(memberid_in integer)
is
-- Define local variables
vCountOut number(2);
vAllowedOut number(2);
vNextDvd number(16);
vAddedInQueue date;
vPlaceInQueue number(5);
BEGIN
-- ensure the member is eligible to take out another DVD at this time
-- See how many movies the member currently has out
select count(*) into vCountOut from rental where memberid = memberid_in and rentalreturneddate is null;
-- See how many movies the member is allowed to have out
select membershiplimitpermonth into vAllowedOut from membership m, member p where m.membershipid = p.membershipid and p.memberid = memberid_in;
IF vCountOut < vAllowedOut then
-- If the number out currently is less than the number allowed out currently
-- Get next DVD in queue available for shipment
select get_nextdvd(memberid_in) into vNextDVD, datedaddedinqueue into vAddedInQueue, rentalpriority into vPlaceInQueue from rentalqueue where memberid = memberid_in;
-- create new record in rental table for this shipment
insert into rental (rentalid, memberid, dvdid, rentalrequestdate, rentalshippeddate) values (rental_seq.nextval, memberid_in, vNextDVD, vAddedInQueue, current_date);
-- decrement dvdquantityon hand in dvd table
update dvd set quantityonhand = quantityonhand - 1 where dvdid = vNextDVD;
-- remove dvd from queue
delete from rentalqueue where memberid = memberid_in and dvdid = vNextDVD ;
-- manage remaining rentalpriority records by decrementing them properly (like in 2.1)
update rentalqueue set rentalpriority = rentalpriority - 1 where memberid = memberid_in and rentalpriority >= vPlaceInQueue;
ELSE
-- If the member already has the maximum number of movies out at this time
RAISE_APPLICATION_ERROR(-20101, 'Maximum number of movies out reached. You are not allowed to check out another movie at this time.');
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('No Data Returned, process failed.');
WHEN TOO_MANY_ROWS THEN
dbms_output.put_line('Too many rows returned into variable, check data and try again.');
WHEN OTHERS THEN
dbms_output.put_line('An unidentified error has occured. Please research issue with procedure.');
END;
/
I am getting the following error even after adding the schema name to the procedure call. I've also tried running it with "execute" and "call" but know those do not work properly.
PROCEDURE PRCRETURNDVD compiled
Errors: check compiler log
19/7 PL/SQL: Statement ignored
19/14 PLS-00905: object SYSTEM.PRCSHIPNEXTDVD is invalid
Any other recommendations on what I'm doing wrong on this one?
There is an issue with the procedure I'm calling but it does compile.
(as outlined in comments)
Fix the compile errors in prcShipNextDVD.
Then the call from the other stored procedure will work.

SIGNAL SQLSTATE Inside Trigger Failing

I have a Product table with 4 columns. 2 columns are price. If the ListPrice column is updated to below a specified amount (StandardCost * 1.2) then the update should fail and the old ListPrice should remain. I am attempting to use a SIGNAL SQLSTATE error to prevent the update from occurring if the criteria are met.
I've been combing Google and tried various variations in the syntax, but I keep hitting the following error while compiling my trigger - "PLS-00103 - Encountered the symbol 'SQLSTATE' when expecting one of the following: := , ( # %"
Any help is greatly appreciated.
CREATE OR REPLACE TRIGGER Product_Price_Check
BEFORE INSERT OR UPDATE OF ListPrice ON Product
FOR EACH ROW
DECLARE
min_price NUMBER(10, 2);
new_price NUMBER(10, 2);
BEGIN
min_price := (:OLD.StandardCost*1.2);
new_price := (:NEW.ListPrice);
IF (new_price < min_price) THEN
-- Rolls back an explicit or implicit transaction to the beginning of the transaction
dbms_output.put_line('the price can’t be below ' || TO_CHAR(min_price));
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insert/update failed';
END IF;
END;
As mustaccio said, you're mixing MySQL syntax with an Oracle trigger. You want raise_application_error:
BEGIN
IF :NEW.ListPrice < (:OLD.StandardCost*1.2) THEN
raise_application_error(-20001,
'the price can’t be below ' || TO_CHAR(:OLD.StandardCost*1.2));
END IF;
END;
/
This won't roll back the transaction, just the update statement. The caller will receive the exception and decide how to handle it - whether to try again, roll back, or commit any other changes already made.
This assumes the old standard cost cannot be null. You might also want to specify a format model for the to_char().
Also don't rely on dbms_output for informing the caller about anything, as you won't know if the caller is looking at or doing anything with the buffer.

first attempt at learning oracle triggers

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.

ORA-02014- How do I update a randomly selected row from a table?

I'm trying to randomly select a card from a table of cards with columns c_value and c_suit using a procedure. After selecting it, the procedure should update that entry's taken field to be 'Y'.
create or replace procedure j_prc_sel_card(p_value OUT number,
p_suit OUT number)
AS
CURSOR CUR_GET_RAND_CARD IS SELECT c_value,
c_suit
FROM (SELECT c_value,
c_suit,
taken
FROM jackson_card
ORDER BY dbms_random.value)
WHERE rownum = 1
FOR UPDATE OF taken;
BEGIN
OPEN CUR_GET_RAND_CARD;
FETCH CUR_GET_RAND_CARD into p_value, p_suit;
UPDATE jackson_card
SET taken = 'Y'
WHERE c_value = p_value
AND c_suit = p_suit;
CLOSE CUR_GET_RAND_CARD;
END;
Then I am trying to get the selected card and output what it is as a start. With this:
SET serveroutput on;
DECLARE v_value number;
v_suit number;
BEGIN
j_prc_sel_card(p_value => v_value,p_suit => v_suit);
DBMS_OUTPUT.PUT_LINE(v_value);
DBMS_OUTPUT.PUT_LINE(v_suit);
END;
/
However i got the error stated in the title and it seems my way of selecting a random card is stopping me from doing an update. Thanks in advance!
Here is a different take on the scenario (I did also address your immediate problem in a different answer).
Given that we really are building a card-dealing program (as opposed to working with a test case for a business scenario) I didn't like the TAKEN column. Updating a table column to mark a transitory state seems wrong. What happens when we want to play another game?
The following solution resolves this by populating an array with all the cards in a random order upfront (the shuffle). The cards are dealt by simply taking the next entry off the stack. The package offers a choice of approach for running out of cards: either throw a user-defined exception or just cycle through the deck again.
create or replace package card_deck is
no_more_cards exception;
pragma exception_init(no_more_cards, -20000);
procedure shuffle;
function deal_one
( p_yn_continuous in varchar2 := 'N')
return cards%rowtype;
end card_deck;
/
create or replace package body card_deck is
type deck_t is table of cards%rowtype;
the_deck deck_t;
card_counter pls_integer;
procedure shuffle is
begin
dbms_random.seed (to_number(to_char(sysdate, 'sssss')));
select *
bulk collect into the_deck
from cards
order by dbms_random.value;
card_counter := 0;
end shuffle;
function deal_one
( p_yn_continuous in varchar2 := 'N')
return cards%rowtype
is
begin
card_counter := card_counter + 1;
if card_counter > the_deck.count()
then
if p_yn_continuous = 'N'
then
raise no_more_cards;
else
card_counter := 1;
end if;
end if;
return the_deck(card_counter);
end deal_one;
end card_deck;
/
Here it is in action. Don't use an open LOOP if you set the continuous dealing mode to Y.
SQL> set serveroutput on
SQL>
SQL> declare
2 my_card cards%rowtype;
3 begin
4 card_deck.shuffle;
5 loop
6 my_card := card_deck.deal_one;
7 dbms_output.put_line ('my card is '||my_card.c_suit||my_card.c_value);
8 end loop;
9 exception
10 when card_deck.no_more_cards then
11 dbms_output.put_line('no more cards!');
12 end;
13 /
my card is HA
my card is H7
my card is DJ
my card is CQ
my card is D9
my card is SK
no more cards!
PL/SQL procedure successfully completed.
SQL>
You may think I'm not dealing with a full deck. You wouldn't be the first to think that ;)
You are using an explicit cursor already, so you don't need the ROWNUM = 1 filter. Try this:
create or replace procedure j_prc_sel_card(p_value OUT number,
p_suit OUT number)
AS
CURSOR CUR_GET_RAND_CARD IS
SELECT c_value,
c_suit,
taken
FROM jackson_card
WHERE taken != 'Y'
ORDER BY dbms_random.value
FOR UPDATE OF taken;
BEGIN
OPEN CUR_GET_RAND_CARD;
FETCH CUR_GET_RAND_CARD into p_value, p_suit;
UPDATE jackson_card
SET taken = 'Y'
WHERE CURRENT OF cur_get_rand_card;
CLOSE CUR_GET_RAND_CARD;
END;
Note the use of WHERE CURRENT OF. This is the most efficient way of locating a row when we are using the FOR UPDATE CLAUSE. Without the use of the NOWAIT clause the cursor will hang if the chosen card is locked by another session. An unlikely scenario perhaps but one worth considering when you move beyond card games and into real scenarios.
Also, remember that for a truly random shuffle you need to call DBMS_RANDOM.SEED() at the start of proceedings.

Resources