Oracle trigger checks previous record before insert/update - oracle

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;

Related

Getting error PLS-003036 wrong number or types of argument in call to =

I had written the below trigger
create or replace trigger my_trigger
Before insert or update on table1
referencing new as new old as old
for each row
declare id number;
cursor id_cnt is
select count(*) from table2 where my_id=:new.my_id;
begin
if :new.my_id is null
then RAISE_APPLICATION_ERROR(-001,"MY_ID should nit be null");
elsif id_cnt=0 then
RAISE_APPLICATION_ERROR(-002,"not a valid id ");
else
select new_id from table2 where my_id=:new.my_id;
if lenght(new_id) <5
then
RAISE_APPLICATION_ERROR(-003,"length is very small ");
END IF;
END IF;
END my_trigger;
At if :new.my_id is null i am getting the below error
error PLS-003036 wrong number or types of argument in call to =
There are 2 conditions needs to be checked first condition i need to check my_id is null or not and second condition need to check the length of new_id before that i am checking if that my_id is already existed in table 2 before inserting into table1
You:
want to SELECT ... INTO rather than using a CURSOR
misspelt LENGTH
need to use ' for string literals and not "; and
need to use -20000 to -20999 for user-defined error numbers.
Like this:
create or replace trigger my_trigger
Before insert or update on table1
referencing new as new old as old
for each row
declare
id number;
id_cnt PLS_INTEGER;
v_new_id table2.new_id%TYPE;
begin
IF :new.my_id is null THEN
RAISE_APPLICATION_ERROR(-20001,'MY_ID should nit be null');
END IF;
select count(*)
INTO id_cnt
from table2
where my_id=:new.my_id;
if id_cnt=0 then
RAISE_APPLICATION_ERROR(-20002,'not a valid id');
else
select new_id
INTO v_new_id
from table2
where my_id=:new.my_id;
if length(v_new_id) < 5 then
RAISE_APPLICATION_ERROR(-20003,'length is very small');
END IF;
END IF;
END my_trigger;
/
db<>fiddle here

Committing concurrent transactions only if all of them succeed

What I want is to execute a procedure ( that will run on Oracle 11g) concurrently, and to commit the whole operation if an only if all the concurrent transactions succeeded.
Two ways of parallel execution that I thought of were DBMS_PARALLEL_EXECUTE and dbms_job.submit(), but as I understand it, in both cases the processes created are running in their separate sessions and each process commits the changes upon termination (or in case of an error can rollback its own changes).
What I would like, is to start parallel processes, wait until each one of them is finished, check if they all were successful and only then commit the changes (or rollback if at least one process failed).
Is the above scenario possible? (And how can it be implemented)
Thanks.
I am curious why this requirement came about; I would probably question whether it was really necessary if this came to me. But if you cannot question the requirement, this is how I would go about it.
-- We need to have a common persistent location for all of the jobs to read
CREATE TABLE test_tab
(
job_name VARCHAR2(30),
status VARCHAR2(30)
)
/
-- The procedure writing to our table must be autonomous so that updates occur
-- without committing the rest of the work
CREATE OR REPLACE PROCEDURE test_log
(
i_job_name IN VARCHAR2,
i_status IN VARCHAR2
) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
MERGE INTO test_tab tgt
USING dual
ON (tgt.job_name = i_job_name)
WHEN MATCHED THEN
UPDATE SET status = i_status
WHEN NOT MATCHED THEN
INSERT VALUES (i_job_name, i_status);
COMMIT;
END test_log;
/
CREATE OR REPLACE PROCEDURE test_proc(i_job_name IN VARCHAR2) IS
l_complete_cnt INTEGER;
l_error_cnt INTEGER;
l_waiting BOOLEAN := TRUE;
BEGIN
-- !!! Your code here !!!
/* -- Uncomment this block to prove the rollback scenario.
IF i_job_name LIKE '%8' THEN
raise_application_error(-20001, 'Throwing an error to prove rollback.');
END IF;*/
test_log(i_job_name, 'COMPLETE');
WHILE l_waiting LOOP
SELECT SUM(CASE WHEN status IN ('COMPLETE', 'COMMITTED') THEN 1 ELSE 0 END)
,SUM(CASE WHEN status = 'ERROR' THEN 1 ELSE 0 END)
INTO l_complete_cnt
,l_error_cnt
FROM test_tab
WHERE REGEXP_LIKE(job_name, 'TEST_JOB_\d');
IF l_complete_cnt = 8 THEN
COMMIT;
test_log(i_job_name, 'COMMITTED');
l_waiting := FALSE;
ELSIF l_error_cnt > 0 THEN
ROLLBACK;
test_log(i_job_name, 'ROLLBACK');
l_waiting := FALSE;
ELSE
dbms_lock.sleep(seconds => 5);
END IF;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
test_log(i_job_name, 'ERROR');
RAISE;
END;
/
-- Begin test section
BEGIN
FOR i IN 1..8 LOOP
dbms_scheduler.create_job('TEST_JOB_'||i
,'PLSQL_BLOCK'
,'BEGIN test_proc(''TEST_JOB_'||i||'''); END;'
,start_date => SYSDATE
,enabled => TRUE);
END LOOP;
END;
/
SELECT * FROM test_tab;
TRUNCATE TABLE test_tab; --the table should be cleared between tests

Oracle trigger on 2 tables : no data found

Please help,I'm trying to allow/ban insertion into a table called 'vol' that has a foreign key (id_av) from another table 'avion'
allow insertion : if avion.etat = 'disponible'
ban it if it is different from 'disponible'
for that I have created this trigger :
create or replace trigger t
before insert on vol
declare etat VARCHAR(10);
BEGIN
select avion.etat into etat
from vol,etat
where avion.id_av = vol.id_av;
IF(etat <> 'disponible')
THEN
RAISE_APPLICATION_ERROR( -20001, 'insertion imposible');
END IF;
END t;
/
the result : the trigger is created but when I tryed to insert in vol it shows me these errors
I've tryed also with JOIN..ON but didn't really worked out
Perhaps something like this?
create or replace trigger t
before insert on vol
for each row --> edited
declare
etat VARCHAR(10);
BEGIN
-- MAX will prevent NO-DATA-FOUND
-- Also, you don't need join - use :NEW.ID_AV which is equal to currently inserted value
select max(avion.etat)
into etat
from avion
where avion.id_av = :new.id_av;
-- NVL because - if SELECT returns, nothing, you can't compare NULL with 'disponible'
IF nvl(etat, 'x') <> 'disponible'
THEN
RAISE_APPLICATION_ERROR( -20001, 'insertion imposible');
END IF;
END t;
/

Check ID Exist and delete record in PL SQL

What I have to do is, whenever a STORE_CODE is entered it will check in the db and delete the store code. SO for that, I have written a procedure which is as below
PROCEDURE DELETE_STORE_INFO
(
P_STORE_CODE IN NVARCHAR2
)
AS
BEGIN
UPDATE TBL_RRSOC_STORE_INFO set ISACTIVE = 'N' where STORE_CODE = P_STORE_CODE;
END DELETE_STORE_INFO;
But here what missing is
what if the user enter the wrong store_code and does the operation so what it will do. how to check with that part ?
I guess something for COUNT wont work at this stage. Kindly suggest
You want to use SQL%ROWCOUNT to find out how many rows were affected by the previous SQL statement:
PROCEDURE DELETE_STORE_INFO
(
P_STORE_CODE IN NVARCHAR2
)
AS
BEGIN
UPDATE TBL_RRSOC_STORE_INFO
SET ISACTIVE = 'N'
WHERE STORE_CODE = P_STORE_CODE;
IF SQL%ROWCOUNT = 0 THEN
-- DBMS_OUTPUT.PUT_LINE( 'Store code does not exist.' );
RAISE_APPLICATION_ERROR( -20000, 'Store code does not exist.' );
END IF;
END DELETE_STORE_INFO;
/
You can use DBMS_OUTPUT.PUT_LINE( string ) to output to the SQL console (if you are calling this from an external language like PHP or Java then you will not see the output and you may not see it in the console if you have SET SERVEROUTPUT OFF).
You could also use RAISE_APPLICATION_ERROR( error_code, error_message ) to raise an exception if something invalid happens.
Alternatively you could return a status in an OUT parameter:
PROCEDURE DELETE_STORE_INFO
(
P_STORE_CODE IN NVARCHAR2,
O_STATUS OUT NUMBER
)
AS
BEGIN
UPDATE TBL_RRSOC_STORE_INFO
SET ISACTIVE = 'N'
WHERE STORE_CODE = P_STORE_CODE;
IF SQL%ROWCOUNT = 0 THEN
o_status := 0;
ELSE
o_status := 1;
END IF;
END DELETE_STORE_INFO;
/
Generally contract for your procedure is: caller gives some store_code and procedure guarantee that there is no active store with such code. What if caller gives the wrong store_code? It means there is no such store, so contract is accomplished. You should do nothing, no more :)
But if you wish to check whether update found the record or not, you can add something like
if sql%notfound then
dbms_output.put_line('There is no such store!');
end if;
immediately after update statement.
Especially for MT: just check simple script
create table t$(id integer);
insert into t$ values(1);
set serveroutput on
begin
update t$ set id = 2 where id = 1;
if sql%notfound
then dbms_output.put_line('#1: not found');
else dbms_output.put_line('#1: found');
end if;
update t$ set id = 4 where id = 3;
if sql%found then
dbms_output.put_line('#2: found');
else dbms_output.put_line('#2: not found');
end if;
end;
/
drop table t$;
My results are
Connected to Oracle Database 12c Enterprise Edition Release 12.1.0.2.0
#1: found
#2: not found
PL/SQL procedure successfully completed
There are something missunderstand in your request: you said you want to delete it but in the code, you just updated the store as incative.
Here is the proceure with both situation. You choose the right one:
CREATE OR REPLACE PROCEDURE DELETE_STORE_INFO (P_STORE_CODE IN NVARCHAR2) AS
n_count number;
BEGIN
select count(1) INTO n_count from TBL_RRSOC_STORE_INFO where STORE_CODE = p_store_code;
if n_count > 0 then
UPDATE TBL_RRSOC_STORE_INFO set ISACTIVE = 'N' where STORE_CODE = P_STORE_CODE;
-- or for deletion
-- DELETE TBL_RRSOC_STORE_INFO set ISACTIVE = 'N' where STORE_CODE = P_STORE_CODE;
else
DBMS_OUTPUT.PUT_LINE('the required store was not found');
end if;
END DELETE_STORE_INFO;

How to create a delete stored procedure making sure it cannot be deleted when it has affiliated records in another table?

I have tables
supplier( supplier_id (pk), name, address) supplier_invoice( supp_invoice-id(pk), balance)
supplier_product (supp_prod_id(pk), Supplier_id(fk), product_id(fk), supp_invoice_id(fk))
I am trying to execute a delete procedure:
set serveroutput on;
create or replace
procedure delete_supp
(d_supplier_id int)
is
v_count int;
begin
select count(*) into v_count from supplier_product where d_supplier_id=supplier_id;
if v_count > 0 then
dbms_output.put_line('Supplier cannot be deleted because there is an existing invoice in the system');
else
begin
delete from supplier where supplier_id=d_supplier_id;
DBMS_OUTPUT.PUT_LINE('Deleted ' || SQL%ROWCOUNT || ' Rows.');
commit;
end;
end if;
Exception
when others then
dbms_output.put_line('Delete failed');
end;
/
This procedure works but rather than checking if the record exists in supplier_product, I want to make sure a supplier cannot be deleted if he has open invoice in the supplier_invoice table. I tried with looping in supplier_invoice table but couldn't make it to work.
Small change to your delete statement
DELETE FROM supplier
WHERE supplier_id = d_supplier_id
AND NOT EXISTS (SELECT 1
FROM supplier_product,
supplier_invoice
WHERE supp_invoice_id = supp_invoice_id
AND supplier_id = d_supplier_id);
IF SQL%ROWCOUNT = 0 THEN
RAISE Invoice_exists_exception;
END IF;
and this will ensure that supplier records will be deleted only if no records with the same supplier_id & the linked invoice id exists in the supplier_product table.
Of course, if the FKs are set up (as you mention) - then it should raise an exception when you're trying to delete..
Update: I used SQLFiddle to build a sample schema/data to show this:
If you have Foreign keys set up, you'll run into a ORA-02292: integrity constraint violated exception which you can trap & handle appropriately.

Resources