Cant figure out this SQL Trigger - sql

so I'm suppose to be making a trigger for my database that will limit how many classes a faculty member can be assigned. If QUALIFIED = 'Y', then they can teach up to three classes. the trouble i'm running into is that I dont know what is wrong with my SQL statement that wont let it be run.
CREATE OR REPLACE trigger "ASSIGN_T1"
BEFORE
INSERT ON "ASSIGN"
FOR EACH ROW
BEGIN
DECLARE
A_COUNT NUMBER;
A_QUALIFY CHAR(2);
SET(SELECT QUALIFY FROM QUALIFY WHERE (FID = :NEW.FID)) AS A_QUALIFY
SET(SELECT COUNT(FID) FROM QUALIFY WHERE (FID = :NEW.FID)) AS A_COUNT
IF (A_QUALIFY = 'Y' AND A_COUNT < 3) THEN
INSERT INTO ASSIGN (FID, CID) VALUES (:NEW.FID, :NEW.CID);
END IF;
END;
The two errors i'm getting are
line 8, position 8 PLS-00103: Encountered the symbol "(" when expecting one of the following: constant exception table long double ref char time timestamp
line 8, position 61 PLS-00103: Encountered the symbol "AS" when expecting one of the following: set

The 1st problem here is that the BEGIN needs to move down below the DECLARE and the variable declarations.
The 2nd problem is the way you're attempting to set those variables. Get rid of those SETs and AS's. In PL/SQL, one valid way to set a variable with the result of a SQL statement is with a SELECT INTO. Like so....
SELECT QUALIFY
INTO A_QUALIFY
FROM QUALIFY
WHERE FID = :NEW.FID;
...and you can do the same for A_COUNT
I won't guarantee everything will work right after you do that, but that's the bare minimum to fix here.
Also, even if doing the above works, watch out for SELECT INTO because you'll get a "no data found" error if there's ever a scenario where you don't already have a FID = :NEW.FID being passed in OR an "exact fetch returns more than requested number of rows" error if you have more than 1 existing record with that FID in your table. You then either have to handle those exceptions, or use a different method of assigning values to your variable (such as declaring the SQL in a cursor and then OPEN cursor, FETCH cursor INTO variable, CLOSE cursor.)
Lastly, I think there may be a problem in your logic. You're asking for the value in QUALIFY for a FID, but then you're asking for the number of records that have that FID. That implies that the FID isn't the primary key on your table, which means there could be different records with the same FID but different values in the QUALIFY field. So if you're going to be using that variable in your logic later on, then that may be a problem, since the same FID can have one record with QUALIFY = 'Y', and another record with QUALIFY = 'N'

You have used the BEGIN after DECLARE part. And am not sure why you are using SET .. AS. We can combine both selects into one and use it in IF condition.
I don't think you can trigger on the same table and do insert at the same time. You will end up with ORA-04088 error.
Instead, you can restrict the insertion by throwing an error.
(my option would be a Foreign Key Constraint over the ASSIGN table)
--Creating Tables
create table ASSIGN (FID number, CID number);
create table QUALIFY (FID number, QUALIFY char);
-- Loading sample data
insert into QUALIFY values (1, 'Y');
insert into QUALIFY values (1, 'Y');
insert into QUALIFY values (1, 'Y');
insert into QUALIFY values (2, 'Y');
insert into QUALIFY values (2, 'Y');
insert into QUALIFY values (3, 'N');
insert into QUALIFY values (4, 'Y');
CREATE OR REPLACE trigger "ASSIGN_T1"
BEFORE
INSERT ON "ASSIGNEE" --< change table name to yours
FOR EACH ROW
DECLARE
A_COUNT NUMBER;
BEGIN
SELECT COUNT(QUALIFY) into A_COUNT FROM QUALIFY WHERE QUALIFY='Y' AND FID = :NEW.FID;
-- If they are qualified and already has 3 classes. They are not allowed/record is not inserted.
IF A_COUNT = 3 THEN
Raise_Application_Error (-20343, 'FID is not Qualified or already has 3 Classes.');
END IF;
END;
/
Test by inserting data into the ASSIGNEE table
-- FID 1 already assigned to 3 classes, should not be allowed any more.
insert into ASSIGNEE values (1,3);
-- See error below
Error report -
ORA-20343: FID is not Qualified or already has 3 Classes.
-- FID 2 has only 2 classes, so allowed to insert.
insert into ASSIGNEE values (2,3);
1 row inserted.

One way to accomplish your goal is to do something like this:
CREATE OR REPLACE TRIGGER ASSIGN_T1
BEFORE INSERT ON ASSIGN
FOR EACH ROW
BEGIN
FOR aRow IN (SELECT q.QUALIFY,
COUNT(*) OVER (PARTITION BY q.FID) AS FID_COUNT
FROM QUALIFY q
WHERE q.FID = :NEW.FID)
LOOP
IF aRow.QUALIFY = 'Y' AND aRow.FID_COUNT < 3 THEN
INSERT INTO ASSIGN (FID, CID) VALUES (:NEW.FID, :NEW.CID);
END IF;
END LOOP;
END ASSIGN_T1;

Related

How to prevent invalid data insert SQL

In my project, I need to check conditions dynamically. To achieve this create table as follows.
CREATE TABLE myconditions
(
conditionid INT IDENTITY PRIMARY KEY CLUSTERED,
minvalue INT,
maxvalue INT,
result INT
)
and there data containing as follows,
insert into MyConditions (MinValue, MaxValue, Result)
values (10, 20, 1), (20, 30, 2), (null, 10, 3), (30, null, 3)
I use this table data to check the range of the age,
declare #age int = 25 --this represents user age
select *
from MyConditions
where #age > isnull(MinValue, #age - 1)
and #age <= isnull(MaxValue, #age)
but now the problem is, suppose if someone inserts an invalid range, such as values (5, 25, 4) i mean this is invalid, because in the database already have (10, 20, 1) this values. when the #age = 15 both condition will be through. so I need to prevent (5, 25, 4) this values addition. if someone needs to add this (5, 25, 4) range, This range values (10, 20, 1) should be deleted.
I insert those data into the database using an ASP.NET MVC application. How can I do this? In my project Is using Oracle. (in this question I used MS SQL sample code, but I need oracle)
This sort of data integrity validation is very difficult to implement in a robust and performative fashion.
For starters, much depends upon the definition of overlapping range. For instance it could be argued that all your sample data ranges are invalid: maxvalue = 10 overlaps with minvalue = 10, assuming bounds are testing with >= and <= which is default. Likewise, null bounds create complexity: if you have an existing range (30, null) is (40,50) valid?
So once you have sorted out your business logic there's the matter of implementing them. In Oracle we can do something with a compound trigger. For each row we store the ID of the inserted / updated row in an array. Then at the end of the statement we loop through the array and query the table in a cross join to compare the date ranges.
create or replace trigger myconditions_trg
for insert or update of minvalue, maxvalue
on myconditions
compound trigger
type condition_array is table of int
index by binary_integer;
conditions condition_array;
procedure validate_range (p_id in int) is
overlapping_range exception;
dummy char(1);
begin
begin
select null into dummy
from myconditions t1
, myconditions t2
where t1.conditionid = p_id
and t2.conditionid != p_id
and t1.minvalue != t2.minvalue
and (
t1.minvalue between t2.minvalue and t2.maxvalue
or
t1.maxvalue between t2.minvalue and t2.maxvalue
)
and rownum = 1;
raise overlapping_range;
exception
when no_data_found then
-- what we're hoping for, no overlaps found
null;
end;
exception
when overlapping_range then
raise_application_error(-20000,
'overlapping range for id #' || p_id);
end validate_range;
procedure validate_ranges is
l_id int;
begin
l_id := conditions.first;
loop
exit when l_id is null;
validate_range (l_id);
l_id := conditions.next(l_id);
end loop;
conditions.delete;
exception
when others then
conditions.delete;
raise;
end validate_ranges;
BEFORE EACH ROW is
begin
-- store id to validate
conditions(:new.conditionid) := 1;
end before each row;
AFTER STATEMENT is
begin
validate_ranges;
end after statement;
end myconditions_trg;
This trigger doesn't attempt to handle multi-user scenarios. To be honest there's not much we can do to prevent two different sessions creating overlapping ranges. The only thing which is guaranteed is to lock the whole table, but that may not be desirable.
If you're interested I have a published a demo on Oracle LiveSQL (free login required, sorry!). Find it here.
You need to use either a trigger or a user-defined function for this.
A simple check constraint can only check the values in a single. Frankly, I think a trigger would be the more common approach.
The exact syntax for either depends on the database -- and you have specified two of them -- so a more detailed answer isn't feasible.

SQL - How to create a trigger for two joined tables which is used for inserting

Ok , so I know that inserting information in a view based on two joined tables is impossible.
In order to do so , I need to create a trigger to insert the information in both tables , when an insert is made in that view.
For example :
CREATE VIEW myJoinedView AS
SELECT name,g.value from students
JOIN grades g on g.id=students.id;
The trigger is not working :
CREATE TRIGGER myTrigger
INSTEAD OF INSERT ON myJoinedView
BEGIN
INSERT INTO students
(name,value)
SELECT i.myJoinedView
FROM inserted i
INNER JOIN grades
ON i.id = grades.id
END myTrigger;
Then I'm trying to insert :
INSERT INTO myJoinedView VALUES ('Alex',10);
I don't know if the syntax is correct , I did not find any helpful documentation on this specific type of trigger.
I'm getting this error:
Error(10,46): PLS-00103: Encountered the symbol "end-of-file" when
expecting one of the following: ( begin case declare end exception
exit for goto if loop mod null pragma raise return select update
while with
<< continue close current delete fetch lock
insert open rollback savepoint set sql execute commit forall merge
pipe purge
Any help will be well received.
Thank you!
You need to either perform the inserts separately with separate single table insert or merge statements or by using a multi table insert (insert all) statement. Assuming you have a sequence to generate the id you are joining on for example this code will work in a very rudimentary way, but has some significant issues:
create table students ( id number primary key
, name varchar2(60));
create table grades( id number not null
, value number
, constraint grades_fk1 foreign key (id) references students(id));
create sequence student_id_seq;
create or replace view studentgrades as
select name, value from students s join grades g on s.id = g.id;
create or replace trigger studentgrades_ii_trg
instead of insert on studentgrades
begin
insert all into students(id, name) values (student_id_seq.nextval, name)
into grades(id, value) values (student_id_seq.nextval, value)
select :new.name name, :new.value value from dual;
end;
/
insert into studentgrades values ('Alex',10);
insert into studentgrades values ('Alex',8);
The BIG issue with the above trigger is that every time a grade is inserted for 'Alex' a new student record for 'Alex' is also created instead of reusing the previous student record for 'Alex'. That's probably not the desired behavior. Instead it should probably just insert a new grade record for Alex. One way to acheive this is for the studentgrades view to include the id column from the students table so you can uniquely identify which student to add the grade to, updating the trigger as needed:
create or replace view studentgrades as
select s.id, name, value from students s join grades g on s.id = g.id;
create or replace trigger studentgrades_ii_trg
instead of insert on studentgrades
declare
newid students.id%type;
begin
if :new.id is null then
newid := student_id_seq.nextval;
else
newid := :new.id;
end if;
insert all when :new.id is null
then into students(id, name) values (id, name)
else into grades(id, value) values (id, value)
select newid id, :new.name name, :new.value value from dual;
end;
/
insert into studentgrades values (null, 'Paul',10);
insert into studentgrades values (student_id_seq.currval, 'Paul',8);
However, now what happens if you try this:
insert into studentgrades values (student_id_seq.currval, 'Mary',10);
In this case the name is effectively ignored and Paul gets a new grade so again this isn't quite right. The question is should Paul's name be updated to Mary, or should a new student record for Mary be created, or should an exception be raised?

Making columns optional in Oracle

I'm having a bit of an Oracle dilemma.
I have a table consisting of 5 columns: one PK, two FKs, one INT value and one Date.
I also have a sequence set up for my PK.
I set up a trigger that replaces the PK by an auto-incremented value and the Date by the current date so that you can enter the values (null, FK, FK, INT, null) but I was wondering if there was a way to modify my trigger so that I can enter simply (FK, FK, INT)? As it stands (obviously) if I enter only 3 values I get the ORA-00947: not enough values error.
CREATE or REPLACE trigger TRIG_new_product
before insert on product
for each row
BEGIN
SELECT sq_product.nextval, sysdate
into :new.productID, :new.productDate
FROM dual;
END TRIG_new_product;
If you don't want to supply values for a column, don't list it in the insert statement:
insert into product
(fk_column, fk_column, int_column)
values
(42, 24, 4224);
The error message "not enough" values has nothing to do with your trigger and probably stems from the fact that you didn't specify the columns in your insert statement. In that case you have to supply a value for each column. You probably did something like this:
insert into product -- no columns specified therefore all are required
values
(42, 24, 4224);
Of course leaving out columns during insert will only work if they are defined as nullable.
Btw: your trigger could be written a bit simpler:
CREATE or REPLACE trigger TRIG_new_product
before insert on product
for each row
BEGIN
:new.productID := sq_product.nextval;
:new.productDate := sysdate;
END TRIG_new_product;

INSERT ALL INTO and Sequence.nextval for a Surrogate Key

I'm trying to insert 40 rows using an INSERT ALL INTO and I'm not certain on how to insert the surrogate key. Here's what I have
BEGIN
INSERT ALL
INTO question(question_id)
VALUES (question_seq.nextval)
END
Now if I add another INTO VALUES then I get a unique constraint violation.
BEGIN
INSERT ALL
INTO question(question_id)
VALUES (question_seq.nextval)
INTO question(question_id)
VALUES (question_seq.nextval)
END
How can I update the sequences nextval value for each INTO VALUES so that I can avoid the unique constraint violation? I assumed that nextval would automatically update itself.
UPDATE: I don't know if this is the best way to handle this but here's the solution I came up with:
first I created a function that returns a value
then I called that function in the id field of the VALUES clause
create or replace
FUNCTION GET_QUESTION_ID RETURN NUMBER AS
num NUMBER;
BEGIN
SELECT UHCL_QUESTIONS_SEQ.nextval
INTO num
FROM dual;
return num;
END GET_QUESTION_ID;
INSERT ALL
INTO question(question_id)
VALUES (GET_QUESTION_ID())
INTO question(question_id)
VALUES (GET_QUESTION_ID())
Being from a SQL Server background, I've always created a trigger on the table to basically emulate IDENTITY functionality. Once the trigger is on, the SK is automatically generated from the sequence just like identity and you don't have to worry about it.
You can use something like this:
insert into question(question_id)
select question_seq.nextval from
(
select level from dual connect by level <= 40
);
Although it's not a very convenient format, especially if there are other columns you want to add. You'd probably need to create another UNION ALL query, and join it by the LEVEL or ROWNUM.
My first thought was to do something like this:
insert into question(question_id)
select question_seq.nextval value from dual
union all
select question_seq.nextval from dual;
But it generates ORA-02287: sequence number not allowed here, due to the restrictions on sequence values.
By the way, are you sure your INSERT ALL works without a subquery? I get the error ORA-00928: missing SELECT keyword, and the diagram from the 11.2 manual implies there must be a subquery:
I don't know if this is the best way to handle this but here's the solution I came up with:
first I created a function that returns a value
then I called that function in the id field of the VALUES clause
create or replace
FUNCTION GET_QUESTION_ID RETURN NUMBER AS
num NUMBER;
BEGIN
SELECT UHCL_QUESTIONS_SEQ.nextval
INTO num
FROM dual;
return num;
END GET_QUESTION_ID;
INSERT ALL
INTO question(question_id)
VALUES (GET_QUESTION_ID())
INTO question(question_id)
VALUES (GET_QUESTION_ID())

SQL to insert only if a certain value is NOT already present in the table?

How to insert a number into the table, only if the table does not already have that number in it?
I am looking for specific SQL code, not really sure how to approach this. Tried several things, nothing's working.
EDIT
Table looks like this:
PK ID Value
1 4 500
2 9 3
So if I am trying to INSERT (ID, Value) VALUES (4,100) it should not try to do it!
If ID is supposed to be unique, there should be a unique constraint defined on the table. That will throw an error if you try to insert a value that already exists
ALTER TABLE table_name
ADD CONSTRAINT uk_id UNIQUE( id );
You can catch the error and do whatever you'd like if an attempt is made to insert a duplicate key-- anything from ignoring the error to logging and re-raising the exception to raising a custom exception
BEGIN
INSERT INTO table_name( id, value )
VALUES( 4, 100 );
EXCEPTION
WHEN dup_val_on_index
THEN
<<do something>>
END;
You can also code the INSERT so that it inserts 0 rows (you would still want the unique constraint in place both from a data model standpoint and because it gives the optimizer more information and may make future queries more efficient)
INSERT INTO table_name( id, value )
SELECT 4, 100
FROM dual
WHERE NOT EXISTS(
SELECT 1
FROM table_name
WHERE id = 4 )
Or you could code a MERGE instead so that you update the VALUE column from 500 to 100 rather than inserting a new row.
Try MERGE statement:
MERGE INTO tbl USING
(SELECT 4 id, 100 value FROM dual) data
ON (data.id = tbl.id)
WHEN NOT MATCHED THEN
INSERT (id, value) VALUES (data.id, data.value)
INSERT INTO YOUR_TABLE (YOUR_FIELD)
SELECT '1' FROM YOUR_TABLE YT WHERE YT.YOUR_FIELD <> '1' LIMIT 1
Of course, that '1' will be your number or your variable.
You can use INSERT + SELECT to solve this problem.