Adding a between date constraint in Oracle SQL with parent/child tables - sql

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;
/

Related

How to include a SUBSELECT in VALUES of INSERT to take values from different row?

I want to make a trigger that will insert a value from a connected row. For example I have a table with 3 rows as below:
I create a trigger that will work once row 3 and 4 are deleted (in this case will be deleted at the same time). And I want to record invnr and extinvnr from row 1 based on idparent=id. I cannot seem to make it work though.
CREATE OR REPLACE TRIGGER LOG_DELETEDPAYMENTS
BEFORE DELETE ON payments
FOR EACH ROW
BEGIN
IF :old.invnr IS NULL THEN
INSERT INTO TABLE_LOG_DELETEDPAYMENTS (table_name, invnr, extinvnr, invdate, transactionid, info, createdby, deleted_by, date_of_delete)
values ('payments', :old.invnr, :old.extinvnr, :old.invdate, :old:transactionid, :old.info, :old.createdby, sys_context('userenv','OS_USER'), SYSDATE);
END IF;
END;
How can I incorporate this into the trigger above?
Try it this way:
create or replace TRIGGER LOG_DELETEDPAYMENTS
BEFORE DELETE ON payments
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
Declare
my_invnr PAYMENTS.INVNR%TYPE;
my_extinvnr PAYMENTS.EXTINVNR%TYPE;
Begin
IF :old.INVNR IS NULL THEN
Select INVNR, EXTINVNR
Into my_invnr, my_extinvnr
From PAYMENTS
Where ID = :old.IDPARENT;
--
INSERT INTO TABLE_LOG_DELETEDPAYMENTS (table_name, invnr, extinvnr, invdate, transactionid, info, createdby, deleted_by, date_of_delete)
values ('payments', my_invnr, my_extinvnr, :old.invdate, :old:transactionid, :old.info, :old.createdby, sys_context('userenv','OS_USER'), SYSDATE);
END IF;
End;
END;
You should select the values of INVNR and EXTINVNR based on ID - IDPARENT relationship and store it in the variables (my_invnr and my_extinvnr).
Those variables are used in INSERT into the log statement.
Because of the Select ... Into statement that is reading the affected table - trigger would fail with table PAYMENTS is mutating error.
To avoid that (to separate transaction from the table) you should Declare the PRAGMA AUTONOMOUS_TRANSACTION.
There will be two rows inserted into LOG as the trigger runs FOR EACH (deleted) ROW.
Regards...
This assumes your are on release 12c or greater of Oracle database.
CREATE OR REPLACE PACKAGE LOG_DELETEDPAYMENTS_PKG
AS
-- need a locally defined type for use in trigger
TYPE t_payments_tbl IS TABLE OF payments%ROWTYPE INDEX BY PLS_INTEGER;
END LOG_DELETEDPAYMENTS_PKG;
CREATE OR REPLACE PACKAGE BODY LOG_DELETEDPAYMENTS_PKG
AS
BEGIN
-- could also put the trigger code here and pass the type as a parameter to a procedure
NULL;
END LOG_DELETEDPAYMENTS_PKG;
CREATE OR REPLACE TRIGGER LOG_DELETEDPAYMENTS_CT
FOR DELETE ON payments
COMPOUND TRIGGER
l_tab LOG_DELETEDPAYMENTS_PKG.t_payments_tbl;
l_count PLS_INTEGER:= 0;
BEFORE EACH ROW IS
BEGIN
-- capture the deletes in local type
l_count := l_count + 1;
l_tab(l_count).invnr := :old.invnr;
l_tab(l_count).extinvnr := :old.extinvnr;
l_tab(l_count).invdate := :old.invdate;
l_tab(l_count).transactionid := :old.transactionid;
l_tab(l_count).info := :old.info;
l_tab(l_count).createdby := :old.createdby;
l_tab(l_count).idparent := :old.idparent;
l_tab(l_count).id := :old.id;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN l_tab.first .. l_tab.COUNT LOOP
IF(l_tab(i).invnr IS NULL) THEN
-- if the invoice number is NULL, then get info from parent
SELECT p.invnr
,p.extinvnr
INTO l_tab(i).invnr
,l_tab(i).extinvnr
FROM TABLE(l_tab) p
WHERE p.id = l_tab(i).idparent;
END IF;
END LOOP;
-- log all deletes
FORALL i IN 1 .. l_tab.COUNT
INSERT INTO LOG_DELETEDPAYMENTS
(table_name, invnr, extinvnr, invdate, transactionid, info, createdby, deleted_by, date_of_delete)
VALUES
('payments', l_tab(i).invnr, l_tab(i).extinvnr, l_tab(i).invdate, l_tab(i).transactionid, l_tab(i).info, l_tab(i).createdby, sys_context('userenv','OS_USER'), SYSDATE);
l_tab.delete;
END AFTER STATEMENT;
END LOG_DELETEDPAYMENTS_CT;

Function using dateadd to return status pl sql

I have a table named Borrowing with columns id, borrow_date and duration. I want to create a function to check the status. If someone wants to borrow the book, they give 2 parameters (v_book and v_date). v_book is the book id they want to borrow and v_date is the date they want to borrow. This function checks whether the book can be borrowed or not.
Example, user input v_book=100, and v_date='5-Jan-2020'. But in the table, the book with id 100, the borrow_date is '4-Jan-2020' and the duration is 3 days. So January 4th plus 3 days is January 7th. So that means the book cannot be borrowed by January 5th.
This is my code so far and I still got an error in the dateadd. I need to write the function using Oracle PL/SQL. Any idea? Thanks!
CREATE OR REPLACE FUNCTION check_status (v_book INT, v_date DATE) RETURN VARCHAR2;
v_duration INT;
v_borrow date;
BEGIN
SELECT duration INTO v_duration FROM Borrowing WHERE id = v_book;
SELECT borrow_date INTO v_borrow FROM Borrowing WHERE id = v_book;
SELECT DATEADD(day, v_duration, v_borrow) AS DateAdd;
IF(v_date<DateAdd) THEN
RETURN 'False';
ELSE RETURN 'True';
END IF;
END;
/
DECLARE
m_book INT:=205;
m_date DATE:='5-JAN-2020';
BEGIN
if(check_status(m_book,m_date)='True') then
dbms_output.put_line('You may borrow the book');
else then dbms_output.put_line('book not available');
END;
/
dateadd isn't an Oracle function.
If you want to add days to a date in Oracle, you simply add the number of days to the date.
E.g. 2 days from now would be sysdate + 2.
N.B. if you are assigning a date to a DATE variable, please explicitly convert strings into dates first, e.g.
m_date DATE := to_date('05/01/2020', 'dd/mm/yyyy');
By forcing a string into a DATE variable, you're forcing an implicit conversion, which uses the NLS_DATE_FORMAT parameter of your session as the format of your string, e.g. Oracle will do the following behind the scenes:
m_date DATE := to_date('5-JAN-2020', <NLS_DATE_FORMAT>);
If your NLS_DATE_FORMAT doesn't match the string you've passed in, you'll get an error. By explicitly converting, you've made your code able to run on any session, regardless of the NLS_DATE_FORMAT setting.
Apart from the many syntax errors, your function will not work as a book can be borrowed many times and you need to check all the times that it has been borrowed to make sure none of them overlap with the instant you want to start borrowing.
You want to check something like this:
CREATE OR REPLACE FUNCTION check_status (
v_book BORROWING.ID%TYPE,
v_date DATE
) RETURN VARCHAR2
IS
v_is_borrowed NUMBER(1,0);
BEGIN
SELECT COUNT(*)
INTO v_is_borrowed
FROM Borrowing
WHERE id = v_book
AND borrow_date <= v_date
AND borrow_date + duration > v_date;
RETURN CASE v_is_borrowed WHEN 0 THEN 'True' ELSE 'False' END;
END;
/
Which, for the sample data:
CREATE TABLE borrowing( id, borrow_date, duration ) AS
SELECT 205, DATE '2020-01-01', 31 FROM DUAL UNION ALL
SELECT 205, DATE '2020-02-10', 10 FROM DUAL;
Then:
BEGIN
IF check_status( 205, DATE '2020-01-05' ) = 'True' THEN
dbms_output.put_line('You may borrow the book');
ELSE
dbms_output.put_line('book not available');
END IF;
END;
/
Outputs:
book not available
db<>fiddle here
You can simplify your function code as follows:
CREATE OR REPLACE FUNCTION check_status (v_book INT, v_date DATE) RETURN VARCHAR2
IS -- this was missing in your code
v_cnt number:= 0;
BEGIN
SELECT count(1)
INTO v_cnt
FROM Borrowing
WHERE id = v_book
AND v_date between borrow_date and borrow_date + duration ;
IF(cnt > 0) THEN
RETURN 'False';
ELSE RETURN 'True';
END IF;
END;
/
Also, you can call this function using the SELECT query as follows:
Select check_status(205, date'2020-01-05')
From dual;
Please note how dates are created in oracle.
Create the database table.
create table BORROWING (ID number(3), BORROW_DATE date, DURATION number(2));
Insert a sample row.
insert into BORROWING values (100, to_date('04-01-2021','DD-MM-YYYY'), 3);
Create the function. (I changed the names and types slightly.)
create or replace function CHECK_STATUS(P_BOOK BORROWING.ID%type,
P_DATE BORROWING.BORROW_DATE%type)
return boolean
is
L_DUMMY number(1);
begin
select 1
into L_DUMMY
from BORROWING
where ID = P_BOOK
and P_DATE between BORROW_DATE and (BORROW_DATE + DURATION);
return false;
exception
when NO_DATA_FOUND then
return true;
end;
/
If the desired borrow date for the desired book falls within a period where the book is already borrowed, then the function returns false.
Test the function. (Again changed the names and types.)
declare
L_BOOK BORROWING.ID%type;
L_DATE BORROWING.BORROW_DATE%type;
begin
L_BOOK := 100;
L_DATE := to_date('05-01-2021','DD-MM-YYYY');
if CHECK_STATUS(L_BOOK, L_DATE) then
DBMS_OUTPUT.PUT_LINE('You may borrow the book.');
else
DBMS_OUTPUT.PUT_LINE('Book not available.');
end if;
end;
/
Of-course the function only checks that the book is available on the intended borrow date. The function does not check whether the book can be borrowed for the intended duration. For that, you would need to check the intended duration also.
Refer to this db<>fiddle
You can put all statements into one SELECT Statement during the creation of the function
CREATE OR REPLACE FUNCTION check_status(
i_book Borrowing.Id%type,
i_date Borrowing.Borrow_Date%type
)
RETURN VARCHAR2 IS
o_borrowed VARCHAR2(5);
BEGIN
SELECT DECODE(SIGN(COUNT(*)),0,'True','False')
INTO v_borrowed
FROM Borrowing
WHERE id = i_book
AND i_date BETWEEN borrow_date AND borrow_date + duration - 1;
RETURN o_borrowed;
END;
/
and then revoke such as
DECLARE
v_book Borrowing.Id%type := 205;
v_date Borrowing.Borrow_Date%type := date'2020-01-05';
BEGIN
IF check_status(v_book, v_date) = 'True' THEN
dbms_output.put_line('You may borrow the book');
ELSE
dbms_output.put_line('book not available');
END IF;
END;
/
where there's no predefined function called DATEADD() in Oracle database.
Alternatively, you can create a PROCEDURE as
CREATE OR REPLACE PROCEDURE check_status(
i_book Borrowing.Id%type,
i_date Borrowing.Borrow_Date%type,
o_borrowed OUT VARCHAR2
) IS
BEGIN
SELECT DECODE(SIGN(COUNT(*)),0,'You may borrow the book','book not available')
INTO o_borrowed
FROM Borrowing
WHERE id = i_book
AND i_date BETWEEN borrow_date AND borrow_date + duration - 1;
END;
/
and print the description which you want out directly as
DECLARE
v_book Borrowing.Id%type := 205;
v_date Borrowing.Borrow_Date%type := date'2020-01-05';
v_borrowed VARCHAR2(50);
BEGIN
check_status(v_book, v_date,v_borrowed);
dbms_output.put_line(v_borrowed);
END;
/

Trigger that restricts by date

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.

Oracle Procedure - Return all rows

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.

PL/SQL Triggers

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;