ORA-04092: cannot ROLLBACK in a trigger - oracle

I created a trigger to raise an error when there is an update that causes the total balance to go below -100. I also need the trigger to stop the update from happening. I am getting the error report but I cannot rollback the update.
CREATE OR REPLACE TRIGGER SOLUTION2
AFTER UPDATE OF balance ON ACCOUNT
DECLARE
balance_sum NUMBER(9,2);
BEGIN
balance_sum := 0;
SELECT SUM(balance)
INTO balance_sum
FROM ACCOUNT
WHERE balance < 0
GROUP BY bank_name
HAVING SUM(balance) < -100;
IF (balance_sum != 0) THEN
RAISE_APPLICATION_ERROR(-20001, 'The total value of balance on all bank accounts located in a bank and such that the balance of the accounts is negative, cannot be less than -100');
END IF;
EXCEPTION
WHEN no_data_found THEN
dbms_output.put_line('Balance is updated!');
WHEN OTHERS THEN
dbms_output.put_line(SQLERRM);
END;
/

Related

How stop insert action in trigger

I am using Oracle Apex.
I need to create a trigger for checking the no. of stock whether < 0.
If the no. of stock < 0 then insert action is stopped then prompt a alert message to user.
Below coding dose not work. what is wrong of my code? please help.
My coding:
CREATE OR REPLACE TRIGGER cw_service_b4_trigger
BEFORE INSERT ON cw_serviceline
FOR EACH ROW
DECLARE
exist_stock number;
BEGIN
select stock into exist_stock from cw_inventory where inv_id = :new.inv_id;
if (exist_stock - :new.quantity) < 0 then
dbms_output.put_line ('Out of Stock');
return;
end if;
END;
/
You haven't thrown any sort of error. All your trigger does is output a message.
return doesn't cause the INSERT to stop, it just causes the trigger logic to return.
Instead you need to raise an error to cause the insert to fail.
if exist_stock < :new.quantity then
raise_application_error(-20000, 'Out of stock');
end if;

Oracle trigger checks previous record before insert/update

I would like to create a trigger that prevents a student from enrolling into a new module if he has any outstanding bills.
studID studNRIC paymentStatus
-------------------------------------
200 F7654672F Non Payment
it would reject the following statement:
INSERT INTO student(studID, studNRIC, paymentStatus)
VALUES (201, 'F7654672F', 'Good');
I've came out with the following trigger but I'm still able to insert a new student.
set define off;
CREATE OR REPLACE TRIGGER reject_new_account
AFTER INSERT OR UPDATE ON Student
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
totover NUMBER(3);
BEGIN
SELECT COUNT (*)
INTO totover
FROM Student
WHERE :NEW.nric = student.nric
AND :NEW.paymentStatus = 'Non Payment';
IF totover > 0 THEN
RAISE_APPLICATION_ERROR ( -20002,
'Student' || :NEW.nric ||
' has outstanding bills' );
END IF;
END;
/
there's seems to be a problem with line 13 AND :NEW.paymentStatus = 'Non Payment';
so how do I go about doing this?
table structure
CREATE TABLE Student(
studID INTEGER NOT NULL,
firstName CHAR(25) NULL,
lastName CHAR(25) NULL,
NRIC CHAR(9) NOT NULL,
paymentStatus CHAR(25) Default 'Good',
CONSTRAINT stud_Pkey PRIMARY KEY (studID),
CONSTRAINT studPaymentStatus_type CHECK (PaymentStatus IN ('Late Payment', 'Non Payment', 'Good'))
);
There are a couple of issues with this trigger.
First if you want to prevent an INSERT you need to use a BEFORE trigger. An AFTER trigger will fire after the insert has successfully completed, by which point it is too late to stop the insert.
Secondly, I'm unsure about what you are trying to achieve with your SQL statement. Since the trigger is attached to the customer table you don't need to do a select on the customer table to access the record data. You can just do:
IF :NEW.badstatus = 'Non Payment'
THEN
RAISE_APPLICATION_ERROR ( -20002,
'Employee ' || :NEW.nric ||
' already has 5 readings' );
END IF;
You probably don't want to check if the new record has a bad status, you want to check if the existing customer records have a bad status. As James mentioned, a BEFORE trigger probably makes more sense, but that depends on what you are trying to do, whether the insert gets rollback or not by the caller, etc.
CREATE OR REPLACE TRIGGER reject_new_account
AFTER INSERT OR UPDATE ON Customer
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
totover NUMBER(3);
BEGIN
SELECT COUNT (*)
INTO totover
FROM Customer c
WHERE c.nric = :NEW.nric
AND c.badstatus = 'Non Payment';
IF totover > 0 THEN
RAISE_APPLICATION_ERROR ( -20002,
'Employee ' || :NEW.nric ||
' already has 5 readings' );
END IF;
END;
/
I've removed the PRAGMA AUTONOMOUS_TRANSACTION; for you and replace it with an exception to handle the issue, can you try if it works.
set define off;
CREATE OR REPLACE TRIGGER reject_new_account
BEFORE INSERT OR UPDATE ON CUSTOMER
FOR EACH ROW
DECLARE
totover CHAR(100);
BEGIN
SELECT distinct badStatus
INTO totover
FROM customer
WHERE :NEW.nric = CUSTOMER.nric;
IF totover = 'Non Payment' THEN
RAISE_APPLICATION_ERROR ( -20003,
'Customer ' || :NEW.firstName||''||:NEW.lastName ||
' cannot create new account due to bad payment' );
END IF;
EXCEPTION
WHEN NO_DATA_FOUND
THEN DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20299)));
END;
/
As I assume you have discovered, you cannot select from the same table that a row-level trigger is defined against; it causes a table mutating exception. You have attempted to get round this by adding the autonomous transaction pragma. Unfortunately, although this works, it is just covering up your mistake in methodology.
In order to properly create this validation using a trigger a procedure should be created to obtain user-specified locks so the validation can be correctly serialized in a multi-user environment.
PROCEDURE request_lock
(p_lockname IN VARCHAR2
,p_lockmode IN INTEGER DEFAULT dbms_lock.x_mode
,p_timeout IN INTEGER DEFAULT 60
,p_release_on_commit IN BOOLEAN DEFAULT TRUE
,p_expiration_secs IN INTEGER DEFAULT 600)
IS
-- dbms_lock.allocate_unique issues implicit commit, so place in its own
-- transaction so it does not affect the caller
PRAGMA AUTONOMOUS_TRANSACTION;
l_lockhandle VARCHAR2(128);
l_return NUMBER;
BEGIN
dbms_lock.allocate_unique
(lockname => p_lockname
,lockhandle => p_lockhandle
,expiration_secs => p_expiration_secs);
l_return := dbms_lock.request
(lockhandle => l_lockhandle
,lockmode => p_lockmode
,timeout => p_timeout
,release_on_commit => p_release_on_commit);
IF (l_return = 1) THEN
raise_application_error(-20001, 'dbms_lock.request Timeout');
ELSIF (l_return = 2) THEN
raise_application_error(-20001, 'dbms_lock.request Deadlock');
ELSIF (l_return = 3) THEN
raise_application_error(-20001, 'dbms_lock.request Parameter Error');
ELSIF (l_return = 5) THEN
raise_application_error(-20001, 'dbms_lock.request Illegal Lock Handle');
ELSIF (l_return not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Unknown Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
This procedure can then be used in a compound trigger (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)
CREATE OR REPLACE TRIGGER reject_new_account
FOR INSERT OR UPDATE ON student
COMPOUND TRIGGER
-- Table to hold identifiers of inserted/updated students
g_studIDs sys.odcinumberlist;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal student table
g_studIDs := g_studIDs();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the inserted/updated students; the payment status may be updated
-- without checking the constraint
IF ( INSERTING
OR ( UPDATING
AND ( :new.studID <> :old.studID
OR :new.NRIC <> :old.NRIC)))
THEN
g_studIDs.EXTEND;
g_studIDs(g_studIDs.LAST) := :new.studID;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_students
IS
SELECT sdt.studID
, sdt.NRIC
FROM TABLE(g_studIDs) sid
INNER JOIN student sdt
ON (sdt.studID = sid.column_value)
ORDER BY sdt.NRIC;
CURSOR csr_constraint_violations
(p_studID student.studID%TYPE
,p_NRIC student.studNRIC%TYPE)
IS
SELECT NULL
FROM student sdt
WHERE sdt.NRIC = p_NRIC
AND sdt.paymentStatus = 'Bad Payment'
AND sdt.studID <> p_studID;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated student there exists another record for
-- the same NRIC with a Bad Payment status. Serialise the constraint for each
-- NRIC so concurrent transactions do not affect each other
FOR r_student IN csr_students LOOP
request_lock('REJECT_NEW_ACCOUNT_' || r_student.NRIC);
OPEN csr_constraint_violations(r_student.studID, r_student.NRIC);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Student ' || r_student.NRIC || ' has Bad Payment status');
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;

Oracle procedure to increase a value to below set amount

I wish to create a stored procedure that can be called to increase a staff member's salary by an amount passed in. It should firstly create a savepoint and then check that the new salary is below a set threshold and rollback to the savepoint if it is above the threshold.
-- Raise the Salary of the Staff memeber passed in by the amount specified
CREATE OR REPLACE PROCEDURE RAISE (Selection IN Staff.StaffID%TYPE, amount IN Number)
IS
BEGIN
SAVEPOINT save;
UPDATE Staff
-- increase the salary by amount
SET Staff.Salary = (Staff.Salary + amount)
-- Where StaffID is the one passed in
WHERE Staff.StaffID = Selection;
-- If the new value for salary is greater than 100000
IF ( (Staff.Salary) > 100000 )
-- Rollback and undo the changes
THEN ROLLBACK TO save;
-- Print out a message stating this is against business rules etc...
END IF;
END;
/
The code above gives me back the following error:
I have tried the above code with (:NEW.Salary > 100000) but that gives me a bad bind error on New.Salary.
Does anyone have any suggestions? I think I could most probably replicate this functionality by just having a trigger before alter on the staff table, but why is it that the above code doesn't work?
Thanks!
I ended up using a procedure to create the save and to raise the salary, and then a function, which checks the salary passed in against the threshold and returns either 0 or 1 to the procedure, which then rollsback to the earlier save if necessary.
-- Raise the Salary of the Staff memeber passed in by the amount specified
CREATE OR REPLACE PROCEDURE Raise1 (Selection IN Staff.StaffID%TYPE, amount IN Number)
-- Procedure that takes in StaffID and an amount to increase the staff salary by
IS
BEGIN
SAVEPOINT save;
UPDATE Staff
-- increase the salary by amount
SET Staff.Salary = (Staff.Salary + amount)
-- Where StaffID is the one passed in
WHERE Staff.StaffID = Selection;
-- Pass control to the chekraise procedure, which will rollback to the
-- SAVEPOINT created above if the wage is above 100000 in accordance with business rules
IF ( CheckRaise(Selection) = 0)
THEN
ROLLBACK TO save;
DBMS_OUTPUT.PUT_LINE('Reverted Raise as wage exceeded 100000.');
END IF;
END;
/
CREATE OR REPLACE Function CheckRaise (Selection IN Staff.StaffID%TYPE)
RETURN NUMBER
IS
-- Create a variable, to hold the salary we're checking
Sal NUMBER(10);
BEGIN
-- Select the Staff.Salary that was just passed in into the sal variable
SELECT Staff.Salary INTO Sal FROM Staff WHERE Staff.StaffID = Selection;
-- If sal is above threshold
IF (Sal >= 100000)
THEN
-- Return 0, which the procedure will interpret as "Rollback"
RETURN 0;
ELSIF (Sal < 100000)
THEN
RETURN 1;
END IF;
EXCEPTION
WHEN OTHERS THEN
raise_application_error(-20001, 'Uh oh Error - '||SQLCODE||' Unexpected error '||SQLERRM);
END;
/

PL/SQL TRIGGER COMPILE ERROR

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.

oracle handling no data from query

I am writing a trigger to check if an employee is allocated to two flights that overlap.
It would be something like:
select flight_id from flight
where arrival_time > :new.departure_time
and departure_time < :new.arrival_time;
if flight_id is empty
[do nothing]
if flight_id exists
[raise application error]
Can anyone help me out in how I would have to code the conditional logic there? This is my first time working with oracle (university coursework).
SELECT ... INTO is your friend. It will raise an exception, you catch it, do nothing, otherwise and raise your own error.
select flight_id into v
from flight
where arrival_time > :new.departure_time
and departure_time < :new.arrival_time;
raise_application_error(-20000, 'not good');
exception
when NO_DATA_FOUND
then return 1
when others
then raise_application_error(-20011,'really good');
DECLARE
V_some_varible NUMBER;
BEGIN
Seclect 1 into v_some_varible from dual where 1 = 0;
EXCEPTION -- exception handlers begin
WHEN NO_DATA_FOUND THEN
INSERT INTO errors (message) VALUES ('no data found');
WHEN OTHERS THEN -- handles all other errors
ROLLBACK;
END;
Note the 1 = 0 to force the no data found exception. This example should work in any Oracle database with the exception of the insert into an errors table.

Resources