“How to fix trigger in oracle pl/sql? - oracle

Problem:
Need a TRIGGER for the Table A which does the following:
Each time an INSERT is made into Table A on column TRT_PROCEDURE value, add 1
to the column 'TRT_INS_COUNT' in the table B. If the TRT_PROCEDURE value does
not exist in the table B, add a row to the table B for the procedure setting
TRT_INS_COUNT to 1.
Each time a DELETE is made on Table A, add 1 to the column TRT_DEL_COUNT in
table B for that Procedure value if it exists in the table B. If it does not
exist in the table B, add a row to the table B for the procedure and set
TRT_DEL_COUNT to 1.
Each time an UPDATE occurs for Column TRT_PROCEDURE in TABLE A, add 1 to the
column TRT_UPD_COUNT in table B. If it is not in the table B, add a row to
the table B for the procedure value and set TRT_UPD_COUNT to 1. If the Column
TRT_PROCEDURE value in TABLE A is changed, add 1 to the TRT_UPD_COUNT for the
old procedure value.
Test by doing several INSERTs, DELETEs, and UPDATEs and then display the table B.
Updates: whenever an value of trt_procedure is inserted into table B, the second same trt_procedure (e.g '88-20') cannot be correctly counted into its designated count field. Not sure where went wrong.
TABLE A
Name Null Type
TRT_ID NOT NULL NUMBER(3)
PAT_NBR NUMBER(4)
PHYS_ID NUMBER(3)
TRT_PROCEDURE VARCHAR2(5)
TRT_DATE DATE
TABLE B
Name Null Type
TRT_PROCEDURE NOT NULL VARCHAR2(5)
TRT_INS_COUNT NUMBER(3)
TRT_DEL_COUNT NUMBER(3)
TRT_UPD_COUNT NUMBER(3)
TEST SAMPLES
INSERT INTO A VALUES (11, 8031,101,'88-20',sysdate );
INSERT INTO A VALUES (12, 5872,101,'60-00',sysdate );
UPDATE A SET trt_procedure = '88-20' WHERE trt_id=6;
/*row trt_id =6 cloumn trt_procedure old value '54-60'
new value '88-20', which means Table B row '88-20' and
row '54-60 both trt_upd_count should add 1*/
DELETE FROM A WHERE trt_id=1;
/*row trt_id =1 cloumn trt_procedure value also '88-20', which means
that Table B row '88-20' trt_del_count should also add 1*/
CREATE OR REPLACE TRIGGER trt_stats_trg
BEFORE INSERT OR UPDATE OR DELETE OF TRT_procedure ON A FOR EACH ROW
BEGIN
IF INSERTING THEN
UPDATE B SET trt_ins_count = trt_ins_count+1
WHERE B.trt_procedure = :new.trt_procedure;
IF SQL%NOTFOUND THEN
INSERT INTO B (trt_procedure, trt_ins_count)
VALUES (:new.trt_procedure, 1);
END IF;
ELSIF UPDATING THEN
UPDATE B SET trt_upd_count = trt_upd_count+1
WHERE B.trt_procedure = :old.trt_procedure;
IF SQL%NOTFOUND THEN
INSERT INTO B (trt_procedure, trt_upd_count)
VALUES (:old.trt_procedure, 1);
END IF;
UPDATE B SET trt_upd_count = trt_upd_count+1
WHERE B.trt_procedure = :new.trt_procedure;
IF SQL%NOTFOUND THEN
INSERT INTO B (trt_procedure, trt_upd_count)
VALUES (:new.trt_procedure, 1);
END IF;
ELSIF DELETING THEN
UPDATE B SET trt_del_count = trt_del_count+1
WHERE B.trt_procedure = :old.trt_procedure;
IF SQL%NOTFOUND THEN
INSERT INTO B (trt_procedure, trt_del_count)
VALUES (:old.trt_procedure, 1);
END IF;
END IF;
END trt_stats_trg;

Here you go, with a few fixes.
The tables:
create table a(
TRT_ID NUMBER(3) NOT NULL ,
PAT_NBR NUMBER(4),
PHYS_ID NUMBER(3),
TRT_PROCEDURE VARCHAR2(5),
TRT_DATE DATE
);
create table b(
TRT_PROCEDURE VARCHAR2(5) NOT NULL,
TRT_INS_COUNT NUMBER(3),
TRT_DEL_COUNT NUMBER(3),
TRT_UPD_COUNT NUMBER(3)
);
And the trigger
CREATE OR REPLACE TRIGGER trt_stats_trg
BEFORE INSERT OR UPDATE OR DELETE ON A FOR EACH ROW
BEGIN
IF INSERTING THEN
UPDATE B SET trt_ins_count = nvl(trt_ins_count,0)+1
WHERE B.trt_procedure = :new.trt_procedure;
IF SQL%NOTFOUND THEN
INSERT INTO B (trt_procedure, trt_ins_count)
VALUES (:new.trt_procedure, 1);
END IF;
ELSIF UPDATING('TRT_PROCEDURE') THEN
if nvl(:old.trt_procedure, 'X') <> nvl(:new.trt_procedure, 'X') then
UPDATE B SET trt_upd_count = nvl(trt_upd_count,0)+1
WHERE B.trt_procedure = :old.trt_procedure;
IF SQL%NOTFOUND THEN
INSERT INTO B (trt_procedure, trt_upd_count)
VALUES (:old.trt_procedure, 1);
END IF;
UPDATE B SET trt_upd_count = nvl(trt_upd_count,0)+1
WHERE B.trt_procedure = :new.trt_procedure;
IF SQL%NOTFOUND THEN
INSERT INTO B (trt_procedure, trt_upd_count)
VALUES (:new.trt_procedure, 1);
END IF;
end if;
ELSIF DELETING THEN
UPDATE B SET trt_del_count = nvl(trt_del_count, 0)+1
WHERE B.trt_procedure = :old.trt_procedure;
IF SQL%NOTFOUND THEN
INSERT INTO B (trt_procedure, trt_del_count)
VALUES (:old.trt_procedure, 1);
END IF;
END IF;
END trt_stats_trg;
/
Key points:
You can use UPDATING('COLUMN_NAME') to detect when the SQL affects a specific column.
When UPDATING, you may wish to check for non-changing updates, where a column with value "ABC" is being updated to "ABC". This often happens in frameworks that simply include all columns in an update even when they are not really changing. Detect this with something similar to if nvl(:old.trt_procedure, 'X') <> nvl(:new.trt_procedure, 'X') then.
Table B does not have default values for the counts, so when the original trigger tried to update table B, it succeeded...in updating NULL to NULL, as that's what happens when you add 1 to NULL. This can be addressed by either assigning a default value of 0 to the columns in the table or by using NVL(trt_upd_count,0) + 1 instead of trt_upd_count + 1.
Finally, watch out for usages of :new and :old. It looks like they are correct in this trigger, but it will depend on your business case.

In the IF INSERTING section of your trigger you have
UPDATE B
SET trt_ins_count = trt_ins_count+1
WHERE :old.trt_procedure = :new.trt_procedure;
When inserting a new row all values in the :OLD pseudorow are NULL and thus your UPDATE will never update anything. I think you meant
UPDATE B
SET trt_ins_count = trt_ins_count+1
WHERE B.trt_procedure = :new.trt_procedure;
Give that a try and see if that helps solve your problem.

Related

using cursor inside cursor to loop through table values in oracle

I have a problem where I am trying to update a value of a table using another table through cursor.
create table A (product varchar2, loc varchar2, qty1 number s_date date);
insert into table A values('123','1',40,sysdate+2);
insert into table A values('123','1',50,sysdate+4);
insert into table A values('124','1',0,sysdate+2);
insert into table A values('124','1',0,sysdate+2);
create table B (p1 varchar2, p2 varchar2,loc2 varchar2, qty2 number, a_date date);
insert into table B values('123','124','1',30,sysdate+1);
insert into table B values('123','124','1',20,sysdate+2);
insert into table B values('123','124','1',50,sysdate+3);
Now the requirement is first sort A and B in ascending order of dates
then
for p1 = product and loc2 = loc
for p2 = product and loc2=loc
if qty1>=qty2 and qty1>0 then
qty1 = qty1-qty2 where product = p1
else
move to next row of table A
next.qty1 = qty1-(qty2-previous.qty1)
then we move on to next row of Table B and check if current qty1 from table A >0 and repeat the same process.I am using oracle 18c.
I am trying to acheive this by using 2 cursors but not really getting it. Is there any other way to implement this? can someone please help?
Here is a code that stores whole a and b table into a collection and makes your calculations with A.qty1 if the conditions are satisfied. I made at the end of the code a dbms_output.put_line of the A table collection because I m not sure I understood right your "pseudo code" where u describe conditions and calculation. Look up the server output and if the calculation done on A table collection are right it is easy to update a table with collection. Also note that this is not a very fast solution we are loading whole two tables into a collection (if your table A or B has million rows this will be executing forever) so try to use where in the select into clause to load only the rows u need for that update.
Sample data:
create table A (product varchar2(10), loc varchar2(10), qty1 number, s_date date);
insert into A values('123','1',40,sysdate+2);
insert into A values('123','1',50,sysdate+4);
create table B (p1 varchar2(10), p2 varchar2(10),loc2 varchar2(10), qty2
insert into B values('123','124','1',30,sysdate+1);
insert into B values('123','124','1',20,sysdate+2);
Pl/SQL code:
set SERVEROUTPUT ON;
declare
type t_A_collection is table of a%rowtype index by pls_integer;
v_A_collection t_A_collection;
type t_B_collection is table of b%rowtype index by pls_integer;
v_B_collection t_B_collection;
v_previous_qty1 a.qty1%type;
begin
select * bulk collect into v_A_collection from a order by s_date asc;
select * bulk collect into v_B_collection from b order by a_date asc;
for i in v_B_collection.first..v_B_collection.last loop
--for j in v_A_collection.first..v_A_collection.last loop
if v_A_collection.exists(i-1) then
if v_B_collection(i).qty2>v_A_collection(i-1).qty1 then
v_A_collection(i).qty1:=v_A_collection(i).qty1-(v_B_collection(i).qty2-v_A_collection(i-1).qty1);
v_A_collection(i-1).qty1:=0;
end if;
else
if v_B_collection(i).p1=v_A_collection(i).product
and v_B_collection(i).loc2=v_A_collection(i).loc then
v_A_collection(i).qty1:= abs(v_A_collection(i).qty1-v_B_collection(i).qty2);
elsif v_B_collection(i).p2=v_A_collection(i).product then
v_A_collection(i).qty1:= abs(v_A_collection(i).qty1-v_B_collection(i).qty2);
end if;
end if;
end loop;
for i in v_A_collection.first..v_A_collection.last loop
dbms_output.put_line(v_A_collection(i).product||' '||v_A_collection(i).loc||' '||v_A_collection(i).qty1||' '||v_A_collection(i).s_date);
end loop;
end;
Server output of collection for table A:
PRODUCT|LOC |QTY1 |S_DATE
-----------------------------
123 1 0 30.04.2022
123 1 40 02.05.2022

Trigger is not working when I'm trying to insert a value into a table

I am trying to make an Insert Trigger for a table called Marks which has id, id_student, id_course, value, data_notation, created_at, updated_at.
I need to make an Update on the old value, if the value I want to insert is higher than the one already exists in the column, and if there are no values in the column you would do an Insert with the new value.
I created the Trigger and there are no compilation errors.
CREATE OR REPLACE TRIGGER insert_value
before INSERT ON Marks
FOR EACH ROW
BEGIN
IF (:OLD.value IS NULL) THEN
DBMS_OUTPUT.PUT_LINE('Inserting.. because value is null');
UPDATE Marks SET value = :NEW.value where id_student = :NEW.id_student;
ELSE
DBMS_OUTPUT.PUT_LINE('Updating old value.. if old value is smaller than the one we want');
IF (:OLD.value < :NEW.value) THEN
UPDATE Marks SET value = :NEW.value where :OLD.id_student = :NEW.id_student;
END IF;
END IF;
END;
I want to change the old value from an existing value 5 to null for a specific id.
update Marks set value = null where id = 692;
select * from Marks where id = 692;
But when I'm trying to insert a value into the table so I can change the value null into 6 via the trigger
INSERT INTO Marks
VALUES (692, 43, 12, 6, '13-02-2018', '13-02-2018', '13-02-2018');
I am receiving an error.
Error report -
SQL Error: ORA-00001: unique constraint (STUDENT.SYS_C007784) violated
00001. 00000 - "unique constraint (%s.%s) violated"
*Cause: An UPDATE or INSERT statement attempted to insert a duplicate key.
For Trusted Oracle configured in DBMS MAC mode, you may see
this message if a duplicate entry exists at a different level.
*Action: Either remove the unique restriction or do not insert the key.
And it prints one time:
Inserting.. because value is null
But when I'm trying to check if the trigger did its job, using:
SELECT * from Marks where id = 692;
It doesn't update anything.
It has to be a trigger triggered by an insert operation. So I can't make the insert into the table, but how else should I write it so it works?
You problem comes from recursive calling the trigger due to the insert. The following would work. It does not catch update statements. It only cares for inserts. If the row exists already the row gets deleted first and the existing value is used for the insert if the existing value is higher.
set lin 20000
drop table marks;
create table Marks(
id number,
id_student number,
id_course number,
value number,
data_notation varchar2(40),
created_at timestamp,
updated_at timestamp,
CONSTRAINT marks#u UNIQUE (id, id_student, id_course)
);
create or replace trigger mark_trigger
before insert on marks
for each row
declare
l_value number;
l_data_notation varchar2(40);
l_created_at timestamp;
begin
select value, data_notation, created_at
into l_value, l_data_notation, l_created_at
from
(select *
from marks
where marks.id = :new.id
and marks.id_student = :new.id_student
and marks.id_course = :new.id_course
order by created_at desc)
where rownum=1;
if l_value is null then
return;
end if;
if l_value > :new.value then
:new.value := l_value;
:new.data_notation := l_data_notation;
:new.created_at := l_created_at;
else
:new.updated_at := systimestamp;
end if;
delete from marks
where marks.id = :new.id
and id_student = :new.id_student
and id_course = :new.id_course;
exception
when no_data_found then
null;
end;
create or replace procedure marks_insert(
i_id number,
i_id_student number,
i_id_course number,
i_value number,
i_data_notation varchar2
)
is
begin
INSERT INTO marks
VALUES (i_id, i_id_student, i_id_course, i_value, i_data_notation, systimestamp, null);
END marks_insert;
begin
delete from marks;
marks_insert(1,1,1,5,'1 first entry');
marks_insert(1,1,1,6,'1 second entry');
marks_insert(1,1,2,3,'2 first entry');
marks_insert(1,1,2,2,'2 second entry');
end;
select * from marks;
Output:
Table dropped.
Table created.
Trigger created.
Procedure created.
PL/SQL procedure successfully completed.
ID ID_STUDENT ID_COURSE VALUE DATA_NOTATION CREATED_AT UPDATED_AT
---------- ---------- ---------- ---------- ---------------------------------------- -------------------------------------------------- --------------------------------------------------
1 1 1 6 1 second entry 07/05/2019 13:31:31.266817 07/05/2019 13:31:31.266928
1 1 2 3 2 first entry 07/05/2019 13:31:31.268032
2 rows selected.
You are inserting into the Marks when you insert into the Marks (the insert statement in the trigger before inserting) and so on in a recursive way. Hence the direct cause of error.

Oracle Trigger to insert /update to another table

Basically, i wanted to create a oracle trigger, which will track the Insert/Updates in a table and i wanted to insert these changed records alone into a new table on a daily basis.This new table data will be using for my daily data refresh (more like a CDC approach)
I was using below code. My expectation was when my CUSTOMER table got INSERT/UPDATED,that record should be available in CUSTOMER_D table.But i am missing something in below code
CREATE OR REPLACE TRIGGER CUST_TRIG AFTER INSERT OR UPDATE ON CUSTOMERS
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW WHEN (OLD.ID <> NEW.ID)
begin
IF INSERTING THEN
begin
INSERT INTO CUSTOMERS_D
(ID, NAME, AGE, ADDRESS, SALARY) values
(:new.ID, :new.NAME, :new.AGE, :new.ADDRESS, :new.SALARY);
-- assuming, there is an unique key on id
exception
when dup_val_on_index then
null;
end;
ELSIF UPDATING THEN
IF :NEW.ID = :OLD.ID THEN
UPDATE CUSTOMERS_D DWT
SET DWT.ID = :NEW.ID,
DWT.NAME = :NEW.NAME,
DWT.AGE = :NEW.AGE,
DWT.ADDRESS = :NEW.ADDRESS,
DWT.SALARY = :NEW.SALARY;
END IF;
MERGE INTO CUSTOMERS_D D
USING DUAL
ON (D.ID = :NEW.ID)
WHEN MATCHED THEN
UPDATE SET D.NAME = :NEW.NAME,
D.AGE = :NEW.AGE,
D.ADDRESS = :NEW.ADDRESS,
D.SALARY = :NEW.SALARY
WHEN NOT MATCHED THEN
INSERT
(D.ID, D.NAME, D.AGE, D.ADDRESS, D.SALARY) VALUES
(:NEW.ID, :NEW.NAME, :NEW.AGE, :NEW.ADDRESS, :NEW.SALARY);
END IF;
end test;
Your trigger in its present form has too many unnecessary constructs which you should get rid of. All you need ( as you confirmed in the comments ) is a single insert statement into customers_d whenever a record gets added or inserted from customers table.
I would recommend you to add another column indicating the time of transaction -MODIFIED_TIME
CREATE OR REPLACE TRIGGER CUST_TRIG
AFTER INSERT OR UPDATE ON CUSTOMERS
FOR EACH ROW
begin
INSERT INTO CUSTOMERS_D
(ID, NAME, AGE, ADDRESS, SALARY,modified_time )
values (:new.ID,
:new.NAME,
:new.AGE,
:new.ADDRESS,
:new.SALARY,
systimestamp);
end ;
/
DEMO
At first glance I can see the one issue in this trigger when new record is inserted the OLD.ID will be NULL so WHEN ((OLD.ID <> NEW.ID) will be FALSE and the trigger wont be invoked on INSERT into CUST_TRIG. Just add following condition:
FOR EACH ROW WHEN ((OLD.ID <> NEW.ID) OR (OLD.ID IS NULL))

Use fields from another table as max-number-rows-with-type constrain in oracle

I have two tables:
CREATE TABLE users (
user_id INT(7) NOT NULL,
restricted_type VARCHAR(64) NOT NULL
)
CREATE TABLE type_restrictions (
name VARCHAR(64) NOT NULL,
restriction INT NOT NULL
)
I want to check on insert, that there are no more than restriction users with restricted_type = type_restriction.name.
At this point I'm inserting data with this query:
INSERT INTO users (user_id, restricted_type) SELECT <id>, <type> FROM DUAL
WHERE NOT EXISTS (
SELECT 1
FROM type_restrictions T
WHERE T.name = <type> AND T.restriction < (
SELECT COUNT(*)
FROM users U
WHERE U.user_id = <id> AND U.restricted_type = <type>)
)
But with two or more parallel queries it is possible to end up with more users with restricted_type than actual restriction for this type.
Is there any way to make such constraint work? (Also, I always insert only one row per query, if it helps)
You cannot use select ... in constraint. You cannot select from table which you are inserting into in normal trigger. What you can do? Materialized view (probably, I am not sure) or compound trigger. Here is my (working) try:
create or replace trigger trg_users_restrict
for insert on users compound trigger
type tt is table of number index by varchar2(5);
vt tt;
i varchar2(5);
v_max int;
before statement is
begin
for r in (select restricted_type, count(1) cnt from users group by restricted_type)
loop
vt(r.restricted_type) := r.cnt;
end loop;
end before statement;
after each row is
begin
begin
vt(:new.restricted_type) := vt(:new.restricted_type) + 1;
exception when no_data_found then
vt(:new.restricted_type) := 1;
end;
end after each row;
after statement is
begin
i := vt.first;
while i is not null loop
select nvl(max(restriction), 0) into v_max
from type_restrictions where name = i;
if vt(i) > v_max then
raise_application_error( -20001,
'maximum number exceeded for restriction type ' || i );
end if;
i := vt.next(i);
end loop;
end after statement;
end trg_users_restrict;
In before statement I grouped data from users table into collection. In after each row I increased proper values in collection for newly inserted row(s). In after statement I check if data in collection exceeds allowed ranges in table type_restrictions.
When two sessions insert concurent data then this which commits last causes exception.

Oracle trigger on varchar values not working

I have 2 tables 'label' and 'musician'
CREATE TABLE label
(labId varchar(10) NOT NULL PRIMARY KEY,
labName varchar(20) NOT NULL
);
CREATE TABLE musician
(musId varchar(10) NOT NULL PRIMARY KEY,
musName varchar(30) NOT NULL,
labId varchar(10) NOT NULL,
CONSTRAINT MusLabel FOREIGN KEY (labId) REFERENCES label(labId)
);
I created a trigger to limit the number of musicians a label can have within a
range of 1 to 5; so that for example a label x cannot have 6 musicians:
CREATE OR REPLACE TRIGGER before_musician_insert
BEFORE INSERT ON musician
FOR EACH ROW
DECLARE
total integer;
BEGIN
SELECT COUNT(*) INTO total
FROM musician, label
WHERE musician.labId=label.labId;
IF (total < 0 OR total > 5)
THEN
DBMS_OUTPUT.PUT_LINE('Invalid');
END IF;
END;
/
When I insert a 6th musician into the table with the same label ID, the insert statement does not 'trigger' the TRIGGER and the 6th value is added to the table.
I don't know how to fix this.
I tried a check constraint but with varchar values, it is not working either.
I appreciate your help.
Your code has multiple issues. For instance, it is not accessing :new. The trigger is on the wrong table. It has no error generation.
I might suggest something like this:
CREATE OR REPLACE TRIGGER before_labels_insert
BEFORE INSERT ON labels
FOR EACH ROW
DECLARE
v_total integer;
user_xcep EXCEPTION;
PRAGMA EXCEPTION_INIT( user_xcep, -20001 );
BEGIN
SELECT COUNT(*) INTO v_total
FROM labels l
WHERE l.labId = :new.labId;
IF (v_total >= 5) THEN
DBMS_OUTPUT.PUT_LINE('Invalid');
RAISE user_xcep
END IF;
END;
Your trigger fires BEFORE the insert, so the sixth entry doesn't exist when the query is executed.
If you want to error on the insert of the 6th entry, you can make it an AFTER trigger (and fire once per statement, rather than FOR EACH ROW).

Resources