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;
/
Related
I'm trying to increase all salaries of the employee table with the following procedure.
The problem is that the salary column is doing something weird, is working like a variable, because is saving the salary of the last employee in the cursor and adding up to the next employee, so, at the end I got an error
ORA-01438: value larger than specified precision allowed for this column
PROCEDURE increase_salaries AS
v_emp NUMBER;
v_sal NUMBER;
BEGIN
FOR r1 IN cur_emps LOOP
v_emp := r1.employee_id;
v_sal := r1.salary;
UPDATE employees_copy
SET
salary = salary + salary;
COMMIT;
-- salary = salary + salary * v_salary_increase_rate;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
print('Error in employee '||v_emp);
END increase_salaries;
Thanks
I knwo that I can use first a SELECT INTO for the actual salary and re-initialized it to 0, but I saw many examples on internet using UPDATE salary = salary + ... and it works but with my code does not work.
If you have a table with:
CREATE TABLE employees_copy (
salary NUMBER(6,2)
);
Then you can put values from -9999.99 to 9999.99 into the column. If you try to set a value that is outside those bounds then you will get the error:
ORA-01438: value larger than specified precision allowed for this column
You can either:
Use a smaller value; or
Increase the precision of the column using:
ALTER TABLE employees_copy MODIFY (salary NUMBER(7,2));
and then you can put values from -99999.99 to 99999.99 into the column.
However, you probably also need to fix your procedure to only update a single employee at each loop iteration (rather than updating all employees every loop iteration):
PROCEDURE increase_salaries AS
BEGIN
FOR r1 IN cur_emps LOOP
UPDATE employees_copy
SET salary = 2 * salary;
WHERE employee_id = r1.employee_id;
END LOOP;
END increase_salaries;
/
Note 1: Don't catch the OTHERS exception. Let the exception happen and then you can debug it.
Note 2: Don't COMMIT in the procedure; if you do then you cannot chain together multiple procedures and ROLLBACK them all if one fails. Instead, COMMIT from wherever you are calling the procedure.
I'm trying to find if there's more than 1 president in my database with a trigger and if yes, raise an error, I'm using hr, the table employees and I have to use the job_id to find it. Here's what my code looks like. Thanks!
CREATE OR REPLACE TRIGGER check_pres
BEFORE INSERT OR DELETE OR UPDATE ON employees
FOR EACH ROW
BEGIN
IF ((employees.job_id = 'AD_PRES') > 1)
THEN RAISE_APPLICATION_ERROR(-12345, 'More than one President in database.');
END IF;
END;
You should use Statement Level Trigger instead of Row Level Trigger by removing FOR EACH ROW expression
CREATE OR REPLACE TRIGGER check_pres
BEFORE INSERT OR DELETE OR UPDATE ON employees
DECLARE
v_cnt int;
BEGIN
SELECT COUNT(*) INTO v_cnt FROM employees WHERE job_id = 'AD_PRES';
IF ( v_cnt > 1 ) THEN
RAISE_APPLICATION_ERROR(-20345, 'More than one President in database.');
END IF;
END;
otherwise you'd get mutating error while getting the count value. Btw, the first argument value for RAISE_APPLICATION_ERROR should be between -20999 and -20000
I am trying to take two parameters. a customer id and a purchase amount. The purchase amount is not in the table i will be referencing. it is going to be compared to the credit limit assigned to said customer id and do a dbms output of ethier the credit is too low for the allowed amount or its fine.
I am having trouble implementing the procedure to take the purchase amount parameter and comparing it to the actual credit limit in the table
create or replace PROCEDURE check_available_credit(
c_cust_id IN demo_customer.custid%TYPE,
c_purchase_amount IN NUMBER
) AS c_credit_check VARCHAR(50);
climit demo_customer.creditlimit%type;
BEGIN
SELECT climit
INTO c_credit_check--PLACE INTO PROCEDURE
FROM demo_customer
WHERE custid = c_cust_id;
if(c_purchase_amount > climit)
THEN dbms_output.put_line('Amount is too high');
elsif(c_purchase_amount < climit)
THEN dbms_output.put_line('Amount is perfect');
COMMIT;
END IF;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END check_available_credit;
I will be using 100 for the custid and 4000 for the purchase amount. that will pull the record of a customer whos credit limit is 5000 so it should report "amount is perfect"
You have used climit in the select statement incorrectly. I'm not sure by the way why you're using the variable c_credit_check.
The below procedure seems to work with the above mentioned change. For the exception block I've added a dbms_output to display the error stack. Rollback is not needed unless you've got other DMLs in your procedure body. Commit should ideally be in the calling section and can be removed if there are no DMLs
Test table / data
create table demo_customer( custid int,creditlimit number );
insert into demo_customer(custid,creditlimit) values (100,5000);
Procedure
CREATE OR REPLACE PROCEDURE check_available_credit(
c_cust_id IN demo_customer.custid%TYPE,c_purchase_amount IN NUMBER
) AS
climit demo_customer.creditlimit%TYPE;
BEGIN
SELECT creditlimit
INTO climit
FROM demo_customer
WHERE custid = c_cust_id;
IF(c_purchase_amount > climit )
THEN dbms_output.put_line('Amount is too high');
ELSIF ( c_purchase_amount < climit ) THEN
dbms_output.put_line('Amount is perfect');
--COMMIT; --not needed if there are no dmls, move this to calling
--block if there are any.
END IF;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK; --not needed unless there's a dml
dbms_output.put_line(dbms_utility.format_error_stack);
END check_available_credit;
Execution
SET SERVEROUTPUT ON
BEGIN
check_available_credit(100,4000);
END;
/
Amount is perfect
PL/SQL procedure successfully completed.
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.
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;