limit the amount of trigger updates for a specific row in oracle - oracle

I want to audit update changes in a specific table, and for that I've created a trigger that tracks every time a row is updated, it then write the updated changes into a new historical table:
create table test (id number generated always as identity,name varchar2(10) default null, school varchar2(10) null);
insert into test (name,school) values ('John','MIT');
insert into test (name,school) values ('Max','Oxford');
create table test_history (id int,name varchar2(10), school varchar2(10));
create or replace trigger test_trigger
after update
of name,school
on test
for each row
begin
insert into test_history
values
(
:old.id,
:new.name,
:new.school
);
end;
/
What I would like to do is to limit the amount a specific row is updated to a certain value. For example, the following update statement can only be executed 10 times:
update test
set
name = 'Jason'
where id = 1;
In this way if I execute the above statement 10 times it should work, but if the execution happens the 11th time it should fail. So the maximum amount of rows of a specific unique id is 10.

Count number of rows in the history table and raise an error if it exceeds value you find appropriate.
SQL> create or replace trigger test_trigger
2 after update
3 of name,school
4 on test
5 for each row
6 declare
7 l_cnt number;
8 begin
9 select count(*) into l_cnt
10 from test_history
11 where id = :new.id;
12
13 if l_cnt <= 10 then
14 insert into test_history
15 values
16 (
17 :old.id,
18 :new.name,
19 :new.school
20 );
21 else
22 raise_application_error(-20000, 'Too many updates');
23 end if;
24 end;
25 /
Trigger created.
Update:
SQL> update test set name = 'Jason' where id = 1;
1 row updated.
<snip>
SQL> update test set name = 'Jason' where id = 1;
1 row updated.
SQL> update test set name = 'Jason' where id = 1;
update test set name = 'Jason' where id = 1
*
ERROR at line 1:
ORA-20000: Too many updates
ORA-06512: at "SCOTT.TEST_TRIGGER", line 17
ORA-04088: error during execution of trigger 'SCOTT.TEST_TRIGGER'
SQL>

Related

Create trigger that does the same as unique static constraint PL/SQL

As a disclaimer, this question is only for my curiosity and for practicing triggers and compound triggers particularly.
I've been trying to replace the UNIQUE constraint with a trigger in order to understand triggers more, but I haven't been successful so far, mainly because of the global variables that I'm not so comfortable with inside the compound triggers.
what I'm trying to do with a trigger :
ALTER TABLE Employee
ADD CONSTRAINT emp_UQ
UNIQUE (id_emp, id_office);
here's what I tried so far (t for type, g for global):
CREATE OR REPLACE TRIGGER BIUUniqueConstraint
FOR INSERT OR UPDATE ON Employee
COMPOUND TRIGGER
TYPE tIdEmpOffice IS TABLE OF Employee.id_emp%TYPE
INDEX BY VARCHAR2(80);
gIdEmpOffice tIdEmpOffice;
TYPE tId_emp IS TABLE OF Employee.id_emp%TYPE;
gId_emp tId_emp;
TYPE tId_office IS TABLE OF Employee.id_office%TYPE;
gId_office tId_office;
BEFORE STATEMENT IS
BEGIN
SELECT e.id_emp, e.id_office
BULK COLLECT INTO gId_emp, gId_office
FROM Employee e
ORDER BY e.id_emp;
FOR j IN 1..gId_emp.COUNT() LOOP
gIdEmpOffice(gId_emp(j)) := gId_office(j);
END LOOP;
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
IF INSERTING THEN
DBMS_OUTPUT.PUT_LINE(gIdEmpOffice(:NEW.id_emp);
END IF;
END BEFORE EACH ROW;
END BIUCompteParti;
/
I have no clue how to move forward with this trigger and would like suggestions and explanations if possible about how to use globale variables to store data and how to use them on a row level.
Here's one option.
Sample table:
SQL> create table employee (id_emp number, id_office number);
Table created.
Trigger:
SQL> create or replace trigger trg_emp_unique
2 for insert or update on employee
3 compound trigger
4 type t_row is record (id_emp number, id_office number);
5 type t_tab is table of t_row;
6 l_tab t_tab := t_tab();
7 l_cnt number;
8
9 before each row is
10 begin
11 l_tab.extend;
12 l_tab(l_tab.last).id_emp := :new.id_emp;
13 l_tab(l_tab.last).id_office := :new.id_office;
14 end before each row;
15
16 after statement is
17 begin
18 for i in l_tab.first .. l_tab.last loop
19 select count(*) into l_cnt
20 from employee
21 where id_emp = l_tab(i).id_emp
22 and id_office = l_tab(i).id_office;
23 if l_cnt > 1 then
24 raise_application_error(-20000, 'Unique constraint violated');
25 end if;
26 end loop;
27 l_tab.delete;
28 end after statement;
29 end trg_emp_unique;
30 /
Trigger created.
SQL>
As you can see, it doesn't contain before statement nor after each row parts; if they aren't used, you don't have to put them into the trigger.
Let's try it:
SQL> insert into employee(id_emp, id_office) values (1, 1);
1 row created.
SQL> insert into employee(id_emp, id_office) values (1, 2);
1 row created.
Re-insert the first combination (1, 1):
SQL> insert into employee(id_emp, id_office) values (1, 1);
insert into employee(id_emp, id_office) values (1, 1)
*
ERROR at line 1:
ORA-20000: Unique constraint violated
ORA-06512: at "SCOTT.TRG_EMP_UNIQUE", line 22
ORA-04088: error during execution of trigger 'SCOTT.TRG_EMP_UNIQUE'
That failed (as it should). Let's update existing rows:
SQL> select * from employee;
ID_EMP ID_OFFICE
---------- ----------
1 1
1 2
SQL> update employee set id_office = 5;
update employee set id_office = 5
*
ERROR at line 1:
ORA-20000: Unique constraint violated
ORA-06512: at "SCOTT.TRG_EMP_UNIQUE", line 22
ORA-04088: error during execution of trigger 'SCOTT.TRG_EMP_UNIQUE'
Kind of works.
You can count whether a paired value for those columns already exists with a statement level trigger such as
CREATE OR REPLACE TRIGGER trg_chk_unique_emp_dept_id
AFTER INSERT ON employee
DECLARE
val INT;
BEGIN
SELECT NVL(MAX(COUNT(*)),0)
INTO val
FROM employee
GROUP BY id_emp, id_office;
IF val > 1 THEN
RAISE_APPLICATION_ERROR(-20304,
'Each employee may be assigned to a department once at most !');
END IF;
END;
/
which will check if an attempt made to insert the same value pairs more than once, and if so, it will hurl with an error message.
Demo

Need to create a trigger that updates without adding same number

I need to create a trigger that activates after i make an update in Table A, registering in an audit log the number that i updated in Table A, but if the number has already been added (example the trigger tries to add 1 when there is a 1 already)it must ignore it and only let the first one.
Example:
Table A updates with 5,5,6,8,4,4
Then the audit log must save 5,6,8,4
The trigger i have already:
CREATE OR REPLACE TRIGGER registro_aeropuerto
AFTER UPDATE ON AEROPUERTO
FOR EACH ROW
DECLARE
A INT;
B INT;
BEGIN
A := table_A_updated_column_value;
SELECT CASE
WHEN EXISTS(SELECT * FROM Audit_log WHERE A = Coordinator)
THEN 1
ELSE 0
END INTO B FROM DUAL;
IF B = 0
THEN
INSERT INTO Audit_log(Coodinator, Date) VALUES (A, trunc(sysdate));
END;
Whenever i try to execute the trigger it gives me the next error:
The symbol ";" has been found when the it was expected:
Sample tables (upd_col_value is column you're updating; you named it "table_A_updated_column_value")
SQL> create table aeropuerto (upd_col_value number);
Table created.
SQL> create table audit_log (coordinator number, datum date);
Table created.
Trigger can be simplified; no need to declare any additional variables nor to check first and insert next; do it in the same select statement:
SQL> create or replace trigger registro_aeropuerto
2 after update on aeropuerto
3 for each row
4 begin
5 insert into audit_log (coordinator, datum)
6 select :new.upd_col_value, sysdate
7 from dual
8 where not exists (select null
9 from audit_log a
10 where a.coordinator = :new.upd_col_value
11 );
12 end;
13 /
Trigger created.
Testing:
SQL> insert into aeropuerto (upd_col_value) values (1);
1 row created.
SQL> select * from audit_log;
no rows selected
There's nothing in the log because nothing was updated. So, let's update it:
SQL> update aeropuerto set upd_col_value = 5;
1 row updated.
SQL> select * from audit_log;
COORDINATOR DATUM
----------- -------------------
5 15.09.2021 07:14:46
SQL>
OK; log now contains a row. Another update:
SQL> update aeropuerto set upd_col_value = 6;
1 row updated.
SQL> select * from audit_log;
COORDINATOR DATUM
----------- -------------------
5 15.09.2021 07:14:46
6 15.09.2021 07:15:37
SQL>
Right; two rows, as 5 was updated to 6. What happens if we update 6 back to 5?
SQL> update aeropuerto set upd_col_value = 5;
1 row updated.
SQL> select * from audit_log;
COORDINATOR DATUM
----------- -------------------
5 15.09.2021 07:14:46
6 15.09.2021 07:15:37
SQL>
Nothing happened; row with coordinator = 5 was in the table already so new row wasn't added.

How do i Solve Oracle Mutation Error from TRIGGER

I Have Two Tables; TBL_EMPDETAILS (empdetails_id, EMP_SALARY) and TBL_SERVICE (empdetails_id, Salary, Date_Appointed). The idea is that when i update the tbl_service (which basically is salary history) it should update TBL_EMPDETAILS to the most recent Salary.
I've created a TRIGGER But i keep getting MUTATION ERROR. From my research i have seen recommended compound triggers but i am unsure. I also tried pragma autonomous_transaction; befor the bgin statement but encountered "DEADLOCK ERROR"
create or replace trigger Update_Salary
before insert or update on "TBL_SERVICE"
for each row
declare
x number ;
y number ;
z date ;
m date;
begin
x := :NEW."SALARY";
y := :NEW."EMPDETAILS_ID";
z := :NEW."DATE_APPOINTED";
Select max(DATE_APPOINTED)
into m From TBL_SERVICE Where Empdetails_id = y ;
IF z >= m
THEN
update tbl_empdetails Set EMP_SALARY = x Where Empdetails_id = y ;
End If;
commit;
end;
I Expect that when i add a row to the TBL_SERVICE for eg. (empdetails_id, Salary, Date_Appointed) = (100, $500 , 20-Jul-2019) it should update the TBL_EMPDETAILS (empdetails_id, EMP_SALARY) to (100, $500)
Mutation Error -ORA-04091
Deadlock Error -ORA-00060
So i Think the COMPOUND TRIGGER LOOKS LIKE THE ROUTE TO GO... I TRIED CODE BELOW BUT IM STILL MISSING SOMETHING :(
create or replace TRIGGER "RDC_HR".Update_Salary
FOR UPDATE OR INSERT ON "RDC_HR"."TBL_SERVICE"
COMPOUND TRIGGER
m date ;
AFTER EACH ROW IS
begin
Select max(DATE_APPOINTED) into m From TBL_SERVICE
Where Empdetails_id = :NEW."EMPDETAILS_ID" ;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
IF (:NEW."DATE_APPOINTED") >= m THEN
update tbl_empdetails Set EMP_SALARY = :NEW."SALARY"
Where Empdetails_id = :NEW."EMPDETAILS_ID" ;
End If;
END AFTER STATEMENT;
end Update_Salary;
How about merge?
SQL> create table tbl_empdetails (empdetails_id number, emp_salary number);
Table created.
SQL>
SQL> create table tbl_service (empdetails_id number, salary number, date_appointed date);
Table created.
SQL>
SQL> create or replace trigger trg_biu_ser
2 before insert or update on tbl_service
3 for each row
4 begin
5 merge into tbl_empdetails e
6 using (select :new.empdetails_id empdetails_id,
7 :new.salary salary,
8 :new.date_appointed date_appointed,
9 (select max(s1.date_appointed)
10 from tbl_service s1
11 where s1.empdetails_id = :new.empdetails_id
12 ) da
13 from dual
14 ) x
15 on (x.empdetails_id = e.empdetails_id)
16 when matched then update set e.emp_salary = :new.salary
17 where :new.date_appointed > x.da
18 when not matched then insert (empdetails_id , emp_salary)
19 values (:new.empdetails_id, :new.salary);
20 end;
21 /
Trigger created.
SQL>
Testing:
SQL> -- initial value
SQL> insert into tbl_service values (1, 100, sysdate);
1 row created.
SQL> -- this is now the highest salary
SQL> insert into tbl_service values (1, 200, sysdate);
1 row created.
SQL> -- this won't be used because date is "yesterday", it isn't the most recent
SQL> insert into tbl_service values (1, 700, sysdate - 1);
1 row created.
SQL> -- this will be used ("tomorrow")
SQL> insert into tbl_service values (1, 10, sysdate + 1);
1 row created.
SQL> -- a new employee
SQL> insert into tbl_service values (2, 2000, sysdate);
1 row created.
SQL>
The final result:
SQL> select * From tbL_service order by empdetails_id, date_appointed;
EMPDETAILS_ID SALARY DATE_APPOINTED
------------- ---------- -------------------
1 700 24.07.2019 15:00:21
1 100 25.07.2019 15:00:08
1 200 25.07.2019 15:00:15
1 10 26.07.2019 15:00:27
2 2000 25.07.2019 15:00:33
SQL> select * from tbl_empdetails order by empdetails_id;
EMPDETAILS_ID EMP_SALARY
------------- ----------
1 10
2 2000
SQL>
There are a few basic issues with the trigger as shown.
First, it contains a COMMIT. There shouldn't be a COMMIT in a trigger, since the transaction is still in flight.
The larger problem is that you are accessing the table on which the trigger was created within the trigger:
Select max(DATE_APPOINTED)
into m From TBL_SERVICE Where Empdetails_id = y ;
A row-level trigger cannot query or modify the base table. This is what is causing the mutating table error.
There are a few approaches to handle this.
If you want to use a trigger, you will need to defer the part that queries the base table to a time after the row-level trigger is complete.
This is done using a statement-level trigger or a compound trigger.
A row-level trigger can communicate "work to do" by storing state in a variable in a package, a following statement-level trigger can then inspect the package variables and do work based on the content.
The compound trigger mechanism is a way of putting the row and statement triggers in one code unit, along with the package bits. It is a way of writing the whole thing with one chunk of code (compound trigger) rather than three (row trigger, package, statement trigger).
Here is a detailed writeup of using Compound Triggers: Get rid of mutating table trigger errors with the compound trigger
As mentioned, moving the code out of triggers and into a stored procedure is certainly an option.

Statement Trigger PL/SQL

I need to create a statement trigger that allows data to be updated in PurchaseStock table only during office hours.
The PurchaseStock table is :
PurchaseStock (StockID, ProductID, QuantityIn, Date)
Note that productId is a foreign key here.
I know how to create a trigger for an update but how do I cater for the hours?
Any help is appreciated.
Here, where I live, off-hours are also Saturday & Sunday (i.e. weekend), so - I'd suggest something like this:
SQL> create table purchase_stock (stock_id number, quantity number);
Table created.
SQL> create or replace trigger trg_biu_pursto
2 before insert or update on purchase_stock
3 declare
4 -- day number (1 = Monday, 2 = Tuesday, ..., 7 = Sunday)
5 l_day number := to_number(to_char(sysdate, 'd'));
6 -- current hour (e.g. now is 13:45 -> l_hour = 13)
7 l_hour number := to_number(to_char(sysdate, 'hh24'));
8 begin
9 if l_day in (6, 7) or
10 l_hour not between 9 and 17
11 then
12 raise_application_error(-20000, 'Non-working time; table not available');
13 end if;
14 end;
15 /
Trigger created.
SQL> insert into purchase_stock values (1, 2);
insert into purchase_stock values (1, 2)
*
ERROR at line 1:
ORA-20000: Non-working time; table not available
ORA-06512: at "SCOTT.TRG_BIU_PURSTO", line 10
ORA-04088: error during execution of trigger 'SCOTT.TRG_BIU_PURSTO'
SQL>
Assuming your office hours between 08:00 and 17:00, you can create such a trigger below, to prevent updating data out of office hour you can use raise_application_error statement :
CREATE OR REPLACE TRIGGER trg_upd_PrcStock
BEFORE UPDATE ON PurchaseStock
FOR EACH ROW
DECLARE
v_hour pls_integer;
BEGIN
select into v_hour extract(hour from cast(sysdate as timestamp)) from dual;
IF v_hour between 8 and 16 THEN
-- trigger code
ELSE
raise_application_error('-20333','You can not perform update out of office hours!');
END IF;
END;

Prevent parallel exection of procedure

I have a table trigger, which calls a procedure when the status change from 2 to 3. The procedure check if the whole group of data(group_id) is in status 3 and then perform some actions.
But now I'm facing the problem that when I set the whole group of data in status 3 at the same time, the procedure get called multiple times and perform this actions multiple times. How can I prevent his? For example with locks
Here is my procedure query:
SELECT COUNT(*)
INTO nResult
FROM ticket
WHERE group_id = nGroupId
AND statusid BETWEEN 0 AND 2;
/* If not all tickets of group in status 3, no action required */
IF nResult != 0 THEN
RETURN;
END IF;
And this is my trigger:
IF (:NEW.STATUSID = 3 AND :OLD.STATUSID = 2) THEN
myprocedure(:NEW.group_id);
END IF;
You probably have a row level trigger, that is fired every time a row is updated; for example:
SQL> create table trigger_table(status number);
Table created.
SQL> insert into trigger_table values (1);
1 row created.
SQL> insert into trigger_table values (2);
1 row created.
SQL> insert into trigger_table values (3);
1 row created.
SQL> create trigger update_trigger
2 after update on trigger_table
3 for each row /* ROW LEVEL */
4 begin
5 dbms_output.put_line('change');
6 end;
7 /
Trigger created.
SQL> set serveroutput on
SQL> update trigger_table set status = 1;
change
change
change
3 rows updated.
You need a table level trigger, fired after every update statement:
SQL> create or replace trigger update_trigger
2 after update on trigger_table
3 begin
4 dbms_output.put_line('change');
5 end;
6 /
Trigger created.
SQL> update trigger_table set status = 1;
change
3 rows updated.
Here you find something more.
As rightly observed by Nicholas Krasnov, in this kind of trigger, considering a set of rows and not a single one, you have not the :new or :old values.
A way to get your needs could be the following, but it's a tricky solution and I'd check it carefully before using in a production environment.
You could create a semaphore table to know if you have to fire the trigger or not, then use two triggers, one at row level, BEFORE update, and one at table level, AFTER update; the row level one checks the values and updates the semaphore table while the table level one, fired after the update, reads the semaphore, calls your procedure, if necessary, then resets the semaphore.
For example:
SQL> create table trigger_table(status number);
Table created.
SQL> insert into trigger_table values (1);
1 row created.
SQL> insert into trigger_table values (2);
1 row created.
SQL> insert into trigger_table values (3);
1 row created.
SQL> create table checkChange (fire varchar2(3));
Table created.
SQL> insert into checkChange values ('NO');
1 row created.
SQL> create or replace trigger before_update_trigger
2 before update on trigger_table
3 for each row /* ROW LEVEL */
4 begin
5 if :new.status = 3 and :old.status = 2 then
6 update checkChange set fire = 'YES';
7 end if;
8 end;
9 /
Trigger created.
SQL> create or replace trigger after_update_trigger
2 after update on trigger_table
3 declare
4 vFire varchar2(3);
5 begin
6 select fire
7 into vFire
8 from checkChange;
9 if vFire = 'YES' then
10 dbms_output.put_line('change');
11 update checkChange set fire = 'NO';
12 end if;
13 end;
14 /
Trigger created.
SQL> update trigger_table set status = 2;
3 rows updated.
SQL> update trigger_table set status = 3;
change
3 rows updated.
SQL>

Resources