So I have several tables and I've created different roles to go with different users so that each can access a portion of the tables.
Right now, whenever I try to SELECT * FROM yaser.enrol; with a coordinator who is meant to see everything, I get the error numeric or value error: character to number conversion error which points to the lines where i'm querying the employee_no to determine the employee role.
Theres 4 overall types of users: student, tutor, lecturer, coordinator.
EDIT ** Added all code.
-- Create policy function to be called when ‘ENROL’ table is accessed under user Yaser.
create or replace function f_policy_enrol (schema in varchar2, tab in varchar2)
return varchar2
as
v_emp_no varchar2(10);
v_student_no varchar2(10);
v_tutor_emp_no varchar2(10);
v_lecturer_emp_no varchar2(10);
v_coord_emp_no varchar2(10);
is_tutor number:=0;
is_lecturer number:=0;
is_coordinator number:=0;
is_student number:=0;
is_employee number:=0;
v_program_code varchar2(10);
v_user varchar2(100);
out_string varchar2(400) default '1=2 ';
-- The return value will be out_string. '1=2' means 'Nothing to access'.
begin
-- get session user
v_user := lower(sys_context('userenv','session_user'));
-- Is the user a student?
begin
SELECT student_no INTO v_student_no FROM student WHERE lower(student_no) = v_user;
is_student:=1;
exception
when no_data_found then
v_student_no := 0;
end;
-- Is the user an employee?
begin
SELECT emp_no INTO v_emp_no FROM employee WHERE lower(emp_no) = v_user;
is_employee:=1;
exception
when no_data_found then
v_emp_no := 0;
end;
-- Query the employee number to determine role.
-- If Tutor.
SELECT MAX(tutor_emp_no) INTO v_tutor_emp_no FROM tutorial WHERE lower(tutor_emp_no) = v_user;
-- If Lecturer.
SELECT MAX(course_coord_emp_no) INTO v_lecturer_emp_no FROM course WHERE lower(course_coord_emp_no) = v_user;
-- If Coordinator.
SELECT MAX(prog_coord_emp_no) into v_coord_emp_no FROM program WHERE lower(prog_coord_emp_no) = v_user;
-- Get role of the employee if the user is an employee.
if v_emp_no != 0 and v_tutor_emp_no is NOT NULL then
-- Employee is a Tutor.
is_tutor := 1;
elsif v_emp_no != 0 and v_lecturer_emp_no is NOT NULL then
-- Employee is Lecturer.
is_lecturer := 1;
elsif v_emp_no != 0 and v_coord_emp_no is NOT NULL then
-- Employee is Coordinator.
is_coordinator := 1;
end if;
-- Create the string to be used as the WHERE clause.
if is_student = 1 then
-- Students are allowed to see their orders only.
out_string := out_string||'or student_no = '''||v_student_no||''' ';
end if;
if is_tutor = 1 then
-- Tutors are allowed to see enrolments of students that they tutor.
---- out_string := out_string||'or student_no in (select student_no from tutorial where tutor_emp_no = '||v_tutor_emp_no||') ';
---- NOT WORKING.
out_string := out_string||'or student_no in (select student_no from tutorial where lower(tutor_emp_no) = v_tutor_emp_no) ';
end if;
if is_coordinator = 1 then
-- The coordinator is allowed to see all records in ENROL (WHERE 1=1 or anything) means all rows.
out_string := out_string||'or 1=1 ';
end if;
return out_string;
end;
/
These are the tables i'm referencing:
CREATE TABLE course
(
course_code varchar(10),
course_title varchar(50),
course_coord_emp_no varchar(10),
primary key (course_code)
);
And - all employees:
CREATE TABLE employee
(
emp_no varchar(10),
name varchar(100)
);
All other tables are basically the same - VARCHARS
Any help would be good! :)
Yaser
The question has evolved a lot and much of this answer was about a missing end if that wasn't relevant to your actual code. To go actually go back to your original question, using or 1=1 as a catch-all is OK even though the other branch of the if if comparing strings - it makes no difference at all. If you really did want to compare strings you can do the same thing:
'or ''x''=''x'' '
... or
'or v_user=v_user '
But you can't compare empty strings as you seem to show in a comment. An empty string, '', is the same as null in Oracle , and you can't equate anything to null, even itself. (The previous check would fail if v_user was null, for the same reason). So another possibility would be:
'or null is null '
Note that the comparator here is is, not =.
None of which addresses why you get the VPD error, as all of those are equivalent really - they all always evaluate to true and it doesn't matter which you use. Anything that effectively ends up as or true would work exactly the same; just a shame that Oracle SQL doesn't understand booleans like that. The datatypes being compared in any other clauses are irrelevant to this clause.
You need to see what the function is actually returning in both cases, verify it's what you expect, and verify that it works as a restriction when you query the table the VPD is against directly.
Related
Given a PL/SQL function which looks a bit like:
Function f(pVar IN VARCHAR2) return VARCHAR2 IS
vs_ret VARCHAR2 := NULL;
BEGIN
select name into vs_ret from people where nickname = pVar;
return vs_ret;
END f;
What happens if people.nickname has no uniqueness constraint? What happens if (when) two people have the same nickname - will it lead to an error or just return the value from first row the statement returns?
This appears to be existing functionality which I'm tweaking, so options are somewhat limited to change everything.
It will throw a predefined TOO_MANY_ROWS (ORA-01422) exception. You can handle the exception like this:
CREATE FUNCTION f(
pVar IN VARCHAR2
) RETURN VARCHAR2
IS
vs_ret VARCHAR2;
BEGIN
SELECT name
INTO vs_ret
FROM people
WHERE nickname = pVar;
RETURN vs_ret;
EXCEPTION
WHEN TOO_MANY_ROWS THEN
RETURN NULL; -- or you could do: RETURN 'Error: Too Many Rows';
WHEN NO_DATA_FOUND THEN
RETURN NULL; -- or you could do: RETURN 'Error: Not Found';
END f;
Or, you can leave the exception unhandled; in which case, the exception will get passed back up the hierarchy of calling blocks and each will get a chance to handle it and if it remains unhandled will terminate the query with a ORA-01422: exact fetch returns more than requested number of rows.
An alternative, if you only want the first name returned regardless of how many matches there actually are, is to add AND ROWNUM = 1 to the WHERE clause of the SELECT query (that way there will never be more than one row returned - although there could still be zero rows returned).
Another alternative, if you really do want multiple values (or no values) returned, is to use BULK COLLECT INTO and a collection:
CREATE FUNCTION f(
pVar IN VARCHAR2
) RETURN SYS.ODCIVARCHAR2LIST
IS
vs_ret SYS.ODCIVARCHAR2LIST;
BEGIN
SELECT name
BULK COLLECT INTO vs_ret
FROM people
WHERE nickname = pVar;
RETURN vs_ret;
END f;
unsure if this is useful to you but in instances where I don't care about too many rows or no data found I change it to a cursor.
that way in the no data found it doesn't go into the loop, and for too many rows it just loops that many times.
/* Formatted on 12/17/2015 8:18:33 AM (QP5 v5.115.810.9015) */
FUNCTION f (pVar IN VARCHAR2)
RETURN VARCHAR2
IS
vs_ret VARCHAR2 := NULL;
BEGIN
FOR c IN (SELECT name
FROM people
WHERE nickname = pVar)
LOOP
vs_ret := c.name;
END LOOP;
RETURN vs_ret;
END f;
You will get an error, that you might want to catch if you know something meaningful to do when the error occurs.
drop table people;
create table people (name varchar2(200), nickname varchar2(200));
insert into people values('name1','nick1');
insert into people values('name1','nick1');
select * from people;
create or replace function f(pVar IN VARCHAR2)
return VARCHAR2
IS
vs_ret people.name%type;
BEGIN
select name into vs_ret from people where nickname = pVar;
return vs_ret;
END f;
select f('nick1') from dual;
--- ==> error
create or replace function f(pVar IN VARCHAR2)
return VARCHAR2
IS
vs_ret people.name%type;
BEGIN
select name into vs_ret from people where nickname = pVar;
return vs_ret;
exception
when TOO_MANY_ROWS then
return null; -- This is no good solution but just to demo it
END f;
select f('nick1') from dual;
-- ==> null as Output
Script Output
Table dropped.
Table created.
1 row created.
1 row created.
NAME
--------------------------------------------------------------------------------
NICKNAME
--------------------------------------------------------------------------------
name1
nick1
name1
nick1
2 rows selected.
Function created.
select f('nick1') from dual
*
Error at line 1
ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at "DB.F", line 6
Function created.
F('NICK1')
--------------------------------------------------------------------------------
1 row selected.
Im trying to create a trigger that restricts the amount a reader can read in a given month.
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from (select *
from Reading
where to_char(DateRead, 'YYYY-MM') = to_char(DateRead, 'YYYY-MM'))
where employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
This restricts the reader to 5 max in total no matter the month, i can't seem to get it to be 5 max each month. any ideas greatly appreciated!
Try to rewrite your trigger like this:
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row
declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from Reading
where DateRead between trunc(sysdate,'MM') and last_day(sysdate)
and employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
You add actual month into yout select and you avoid unnecessary date conversion.
I don't understand the condition
if :old.employeeid = :new.employeeid then
Does it mean, the trigger should not fire on updates? In this case it is better to make trigger only for insert or use clause if inserting then...
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 not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
To have the least impact on scalability the serialization should be done at the finest level, which for this constraint is per employeeid and month. Types may be used in order to create variables to store this information for each row before the constraint is checked after the statement has completed. These types can be either defined in the database or (from Oracle 12c) in package specifications.
CREATE OR REPLACE TYPE reading_rec
AS OBJECT
(employeeid NUMBER(10) -- Note should match the datatype of reading.employeeid
,dateread DATE);
CREATE OR REPLACE TYPE readings_tbl
AS TABLE OF reading_rec;
The procedure and types 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 too_many_readings
FOR INSERT OR UPDATE ON reading
COMPOUND TRIGGER
-- Table to hold identifiers of inserted/updated readings
g_readings readings_tbl;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal readings table
g_readings := readings_tbl();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the inserted/updated readings
IF ( INSERTING
OR :new.employeeid <> :old.employeeid
OR :new.dateread <> :old.dateread)
THEN
g_readings.EXTEND;
g_readings(g_readings.LAST) := reading_rec(:new.employeeid, :new.dateread);
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_readings
IS
SELECT DISTINCT
employeeid
,trunc(dateread,'MM') monthread
FROM TABLE(g_readings)
ORDER BY employeeid
,trunc(dateread,'MM');
CURSOR csr_constraint_violations
(p_employeeid reading.employeeid%TYPE
,p_monthread reading.dateread%TYPE)
IS
SELECT count(*) readings
FROM reading rdg
WHERE rdg.employeeid = p_employeeid
AND trunc(rdg.dateread, 'MM') = p_monthread
HAVING count(*) > 5;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated readings there exists more than
-- 5 readings for the same employee in the same month. Serialise the
-- constraint for each employeeid so concurrent transactions do not
-- affect each other
FOR r_reading IN csr_readings LOOP
request_lock('TOO_MANY_READINGS_'
|| r_reading.employeeid
|| '_' || to_char(r_reading.monthread, 'YYYYMM'));
OPEN csr_constraint_violations(r_reading.employeeid, r_reading.monthread);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Employee ' || r_reading.employeeid
|| ' now has ' || r_constraint_violation.readings
|| ' in ' || to_char(r_reading.monthread, 'FMMonth YYYY'));
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;
You need to set the month you are looking at, so if you are considering the current month, make the inner query read like this:
( select * from Reading
where to_char(DateRead,'YYYY-MM') = to_char(DateRead,'YYYY-MM')
and to_char(sysdate,'YYYY-MM') = to_char(DateRead,'YYYY-MM'))
that way it will always compare to the current month and should move as your date moves.
I have two tables Study and Case. The Study is the parent table and Case is the child table. I have to add a constraint such that the CASE_DATE in the Case table is within it's parent table Study's START_DATE and END_DATE.
Study
-------
ID
START_DATE
END_DATE
Case
-----
ID
STUDY_ID
CASE_DATE
The solution is much simpler:
CREATE OR REPLACE TRIGGER check_date
BEFORE UPDATE OR INSERT ON CASE
FOR EACH ROW
DECLARE
StartDate STUDY.START_DATE%TYPE;
EndDate STUDY.END_DATE%TYPE;
BEGIN
SELECT START_DATE, END_DATE
INTO StartDate, EndDate
FROM STUDY
WHERE ID = :NEW.STUDY_ID; -- Assuming ID is the primary key, i.e. unique
IF NEW.STUDY_ID NOT BETWEEN StartDate AND EndDate THEN
raise_application_error(-20001, 'Study date not in valid range');
END IF;
END;
/
However, there are some pre-requisites:
ID in table STUDY is a unique key
START_DATE and END_DATE in table STUDY must not change after insert into table CASE, otherwise you have to write another trigger also for table STUDY
You have a foreign key constraint from STUDY.ID to CASE.STUDY_ID
As long as these pre-requisites are present the trigger should work.
In case you have to do this with a constraint you can do it also. Create a function that checks the date, e.g.
create or replace function IsDateValid(StudyId in number, CaseDate in date)
return boolean is
declare
StartDate STUDY.START_DATE%TYPE;
EndDate STUDY.END_DATE%TYPE;
BEGIN
SELECT START_DATE, END_DATE
INTO StartDate, EndDate
FROM STUDY
WHERE ID = StudyId;
return CaseDate BETWEEN StartDate AND EndDate;
END;
/
Then create the constraint:
ALTER TABLE case ADD CONSTRAINT check_date CHECK (IsDateValid(STUDY_ID, CASE_DATE));
Assuming that the basic referential integrity is being enforced through a standard foreign key constraint, and that no columns are allowed to be NULL.
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 not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
This procedure can then be used in two compound triggers (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)
CREATE OR REPLACE TRIGGER cases_exist
FOR UPDATE ON study
COMPOUND TRIGGER
-- Table to hold identifiers of updated studies (assuming numeric)
g_ids sys.odcinumberlist;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal study table
g_ids := sys.odcinumberlist();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the updated studies
IF ( :new.start_date <> :old.start_date
OR :new.end_date <> :old.end_date)
THEN
g_ids.EXTEND;
g_ids(g_ids.LAST) := :new.id;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_studies
IS
SELECT DISTINCT
sty.column_value id
FROM TABLE(g_ids) sty
ORDER BY sty.column_value;
CURSOR csr_constraint_violations
(p_id study.id%TYPE)
IS
SELECT NULL
FROM study sty
INNER JOIN case cse
ON ( cse.study_id = sty.id
AND cse.case_date NOT BETWEEN sty.start_date AND sty.end_date)
WHERE sty.id = p_id;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any updated study there exists a case outside the start and
-- end dates. Serialise the constraint for each study id so concurrent
-- transactions do not affect each other
FOR r_study IN csr_studies LOOP
request_lock('STUDY_CASES_' || r_study.id);
OPEN csr_constraint_violations(r_study.id);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Study ' || r_study.id || ' has cases outside its dates');
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;
/
CREATE OR REPLACE TRIGGER study_dates
FOR INSERT OR UPDATE ON case
COMPOUND TRIGGER
-- Table to hold identifiers of studies (assuming numeric)
g_study_ids sys.odcinumberlist;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal study table
g_study_ids := sys.odcinumberlist();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the updated studies
IF ( INSERTING
OR :new.study_id <> :old.study_id
OR :new.case_date <> :old.case_date)
THEN
g_study_ids.EXTEND;
g_study_ids(g_study_ids.LAST) := :new.study_id;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_studies
IS
SELECT DISTINCT
sty.column_value id
FROM TABLE(g_study_ids) sty
ORDER BY sty.column_value;
CURSOR csr_constraint_violations
(p_id study.id%TYPE)
IS
SELECT NULL
FROM study sty
INNER JOIN case cse
ON ( cse.study_id = sty.id
AND cse.case_date NOT BETWEEN sty.start_date AND sty.end_date)
WHERE sty.id = p_id;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any updated case it is now outside the start and end dates of
-- the study. Serialise the constraint for each study id so concurrent
-- transactions do not affect each other
FOR r_study IN csr_studies LOOP
request_lock('STUDY_CASES_' || r_study.id);
OPEN csr_constraint_violations(r_study.id);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Study ' || r_study.id || ' has cases outside its dates');
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;
/
I am trying to make a Library Information System. I have a table called Borrower(borrower_id: number, name: varchar2(30), status: varchar2(20)). 'status' can be either 'student' or 'faculty'.
I have a restriction that a maximum of 2 books can be issued to a student at any point of time, and 3 to a faculty. How do I implement it using triggers?
This is a homework question. But I've tried hard to come up with some logic. I am new to SQL so this might be easy for you lot but not for me.
I am new to stackexchange, so sorry if I've violated some rules/practices.
I expect that you would maintain a count on the borrower table of the number of books borrowed, and modify it via a trigger when a book is borrowed and when it is returned. Presumably you also have a table for the books being borrowed by the user, and the trigger would be placed on that take.
A constraint on the books_borrowed column could raise an error if the count of borrowed books exceeds 2.
This is a pretty old question, but I found it very useful as I'm also a PL/SQL beginner. There are two approaches to solve the problem and the one you want to use depends on the Oracle DB version.
For older version use a combination of a trigger and a package as below.
CREATE OR REPLACE TRIGGER trg_borrower
BEFORE INSERT OR UPDATE ON borrower
FOR EACH ROW
DECLARE
v_count NUMBER := 0;
BEGIN
v_count := borrower_pkg.count_rows(:NEW.borrower_id, :NEW.name, :NEW.status);
IF :NEW.status = 'student' AND v_count = 2 THEN
RAISE_APPLICATION_ERROR(-20000, 'Error - student');
ELSIF :NEW.status = 'faculty' AND v_count = 3 THEN
RAISE_APPLICATION_ERROR(-20001, 'Error - faculty');
END IF;
END;
/
CREATE OR REPLACE PACKAGE borrower_pkg AS
FUNCTION count_rows(p_id IN borrower.borrower_id%TYPE,
p_name IN borrower.NAME%TYPE,
p_status IN borrower.status%TYPE) RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY borrower_pkg AS
FUNCTION count_rows(p_id IN borrower.borrower_id%TYPE,
p_name IN borrower.NAME%TYPE,
p_status IN borrower.status%TYPE) RETURN NUMBER AS
v_count NUMBER := 0;
BEGIN
SELECT COUNT(*) INTO v_count
FROM borrower
WHERE borrower_id = p_id AND NAME = p_name AND status = p_status;
RETURN v_count;
END count_rows;
END borrower_pkg;
/
For Oracle 10g and above you can use a compound trigger.
CREATE OR REPLACE TRIGGER trg_borrower_comp
FOR INSERT OR UPDATE ON borrower
COMPOUND TRIGGER
CURSOR c_borrower IS
SELECT b1.borrower_id
FROM borrower b1
WHERE EXISTS (SELECT 'x'
FROM borrower b2
WHERE b2.status = 'student' AND b1.borrower_id = b2.borrower_id
GROUP BY borrower_id HAVING COUNT(*) = 2)
OR
EXISTS (SELECT 'x'
FROM borrower b3
WHERE status = 'faculty'AND b1.borrower_id = b3.borrower_id
GROUP BY borrower_id HAVING COUNT(*) = 3);
TYPE t_borrower_count IS TABLE OF borrower.borrower_id%type;
v_borrower_count t_borrower_count;
BEFORE STATEMENT IS
BEGIN
OPEN c_borrower;
FETCH c_borrower BULK COLLECT INTO v_borrower_count;
CLOSE c_borrower;
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
IF :NEW.borrower_id MEMBER OF v_borrower_count THEN
RAISE_APPLICATION_ERROR(-20000, 'Error - ' || :NEW.status);
END IF;
END BEFORE EACH ROW;
END;
SET SERVEROUTPUT ON
DECLARE
v_student_id NUMBER := &sv_student_id;
v_section_id NUMBER := 89;
v_final_grade NUMBER;
v_letter_grade CHAR(1);
BEGIN
SELECT final_grade
INTO v_final_grade
FROM enrollment
WHERE student_id = v_student_id
AND section_id = v_section_id;
CASE -- outer CASE
WHEN v_final_grade IS NULL THEN
DBMS_OUTPUT.PUT_LINE ('There is no final grade.');
ELSE
CASE -- inner CASE
WHEN v_final_grade >= 90 THEN v_letter_grade := 'A';
WHEN v_final_grade >= 80 THEN v_letter_grade := 'B';
WHEN v_final_grade >= 70 THEN v_letter_grade := 'C';
WHEN v_final_grade >= 60 THEN v_letter_grade := 'D';
ELSE v_letter_grade := 'F';
END CASE;
-- control resumes here after inner CASE terminates
DBMS_OUTPUT.PUT_LINE ('Letter grade is: '||v_letter_grade);
END CASE;
-- control resumes here after outer CASE terminates
END;
the above code I have taken from the book "Oracle PL/SQL by Example, 4th Edition 2009" my problem is when I enter a student_id not present in the table it returns me the following error
Error report: ORA-01403: no data found
ORA-06512: at line 7
01403. 00000 - "no data found"
*Cause:
*Action:
but according to the book it should have returned a null value and then follow the case flow.
When you are selecting INTO a variable and there are no records returned you should get a NO DATA FOUND error. I believe the correct way to write the above code would be to wrap the SELECT statement with it's own BEGIN/EXCEPTION/END block. Example:
...
v_final_grade NUMBER;
v_letter_grade CHAR(1);
BEGIN
BEGIN
SELECT final_grade
INTO v_final_grade
FROM enrollment
WHERE student_id = v_student_id
AND section_id = v_section_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_final_grade := NULL;
END;
CASE -- outer CASE
WHEN v_final_grade IS NULL THEN
...
There is an alternative approach I used when I couldn't rely on the EXCEPTION block at the bottom of my procedure. I had variables declared at the beginning:
my_value VARCHAR := 'default';
number_rows NUMBER := 0;
.
.
.
SELECT count(*) FROM TABLE INTO number_rows (etc.)
IF number_rows > 0 -- Then obtain my_value with a query or constant, etc.
END IF;
Might be worth checking online for the errata section for your book.
There's an example of handling this exception here http://www.dba-oracle.com/sf_ora_01403_no_data_found.htm
Your SELECT statement isn't finding the data you're looking for. That is, there is no record in the ENROLLMENT table with the given STUDENT_ID and SECTION_ID. You may want to try putting some DBMS_OUTPUT.PUT_LINE statements before you run the query, printing the values of v_student_id and v_section_id. They may not be containing what you expect them to contain.
This data not found causes because of some datatype we are using .
like select empid into v_test
above empid and v_test has to be number type , then only the data will be stored .
So keep track of the data type , when getting this error , may be this will help