Before insert trigger - oracle

I want to disable in before insert trigger inserting into table when some condition is true
create or replace trigger CHECK_FOR_MAX_ENTRANTS
before insert on application
declare
entrants_count number;
max_entrants number;
begin
select count(*) into entrants_count from application
where id_speciality = :new.id_speciality;
select max_students_number into max_entrants from speciality s
where s.id_speciality = :new.id_speciality;
IF entrants_count >= max_entrants THEN
**disable this insert**
end;
How can i do this?

Assuming you're talking about Oracle, then, in place of disable this insert you could:
IF entrants_count >= max_entrants THEN
raise_application_error(-21010, 'Max number of Entrants Reached');
END IF;
See: http://download.oracle.com/docs/cd/B19306_01/appdev.102/b14261/errors.htm#sthref2006
EDIT: It's generally a bad idea to have inserts fail silently (what you're asking for). You also may run into mutating table errors if you try to delete the record in an after insert trigger.
Instead, just don't insert the record to begin with.
One way you could probably achieve this is to add something like this to the end of your insert statement:
WHERE EXISTS SELECT null FROM
(SELECT COUNT(*) entrants_count FROM application
WHERE id_speciality = :new.id_speciality) a,
(SELECT max_students_number max_entrants
FROM speciality WHERE id_speciality = :new.id_speciality) s
WHERE a.entrants_count < s.max_entrants
This should only execute the insert statement when entrants_count < max_entrants (what you want).

Try raising an error:
IF entrants_count >= max_entrants THEN
raise_application_error(-20001, 'Cannot perform this insert!');
END IF;

Related

Trying to create a trigger to check if there's more than 1 president in my database

I'm trying to find if there's more than 1 president in my database with a trigger and if yes, raise an error, I'm using hr, the table employees and I have to use the job_id to find it. Here's what my code looks like. Thanks!
CREATE OR REPLACE TRIGGER check_pres
BEFORE INSERT OR DELETE OR UPDATE ON employees
FOR EACH ROW
BEGIN
IF ((employees.job_id = 'AD_PRES') > 1)
THEN RAISE_APPLICATION_ERROR(-12345, 'More than one President in database.');
END IF;
END;
You should use Statement Level Trigger instead of Row Level Trigger by removing FOR EACH ROW expression
CREATE OR REPLACE TRIGGER check_pres
BEFORE INSERT OR DELETE OR UPDATE ON employees
DECLARE
v_cnt int;
BEGIN
SELECT COUNT(*) INTO v_cnt FROM employees WHERE job_id = 'AD_PRES';
IF ( v_cnt > 1 ) THEN
RAISE_APPLICATION_ERROR(-20345, 'More than one President in database.');
END IF;
END;
otherwise you'd get mutating error while getting the count value. Btw, the first argument value for RAISE_APPLICATION_ERROR should be between -20999 and -20000

Statement level trigger to enforce a constraint

I am trying to implement a statement level trigger to enforce the following "An applicant cannot apply for more than two positions in one day".
I am able to enforce it using a row level trigger (as shown below) but I have no clue how to do so using a statement level trigger when I can't use :NEW or :OLD.
I know there are alternatives to using a trigger but I am revising for my exam that would have a similar question so I would appreciate any help.
CREATE TABLE APPLIES(
anumber NUMBER(6) NOT NULL, /* applicant number */
pnumber NUMBER(8) NOT NULL, /* position number */
appDate DATE NOT NULL, /* application date*/
CONSTRAINT APPLIES_pkey PRIMARY KEY(anumber, pnumber)
);
CREATE OR REPLACE TRIGGER app_trigger
BEFORE INSERT ON APPLIES
FOR EACH ROW
DECLARE
counter NUMBER;
BEGIN
SELECT COUNT(*) INTO counter
FROM APPLIES
WHERE anumber = :NEW.anumber
AND to_char(appDate, 'DD-MON-YYYY') = to_char(:NEW.appDate, 'DD-MON-YYYY');
IF counter = 2 THEN
RAISE_APPLICATION_ERROR(-20001, 'error msg');
END IF;
END;
You're correct that you don't have :OLD and :NEW values - so you need to check the entire table to see if the condition (let's not call it a "constraint", as that term has specific meaning in the sense of a relational database) has been violated:
CREATE OR REPLACE TRIGGER APPLIES_AIU
AFTER INSERT OR UPDATE ON APPLIES
BEGIN
FOR aRow IN (SELECT ANUMBER,
TRUNC(APPDATE) AS APPDATE,
COUNT(*) AS APPLICATION_COUNT
FROM APPLIES
GROUP BY ANUMBER, TRUNC(APPDATE)
HAVING COUNT(*) > 2)
LOOP
-- If we get to here it means we have at least one user who has applied
-- for more than two jobs in a single day.
RAISE_APPLICATION_ERROR(-20002, 'Applicant ' || aRow.ANUMBER ||
' applied for ' || aRow.APPLICATION_COUNT ||
' jobs on ' ||
TO_CHAR(aRow.APPDATE, 'DD-MON-YYYY'));
END LOOP;
END APPLIES_AIU;
It's a good idea to add an index to support this query so it will run efficiently:
CREATE INDEX APPLIES_BIU_INDEX
ON APPLIES(ANUMBER, TRUNC(APPDATE));
dbfiddle here
Best of luck.
Your rule involves more than one row at the same time. So you cannot use a FOR ROW LEVEL trigger: querying on APPLIES as you propose would hurl ORA-04091: table is mutating exception.
So, AFTER statement it is.
CREATE OR REPLACE TRIGGER app_trigger
AFTER INSERT OR UPDATE ON APPLIES
DECLARE
cursor c_cnt is
SELECT 1 INTO counter
FROM APPLIES
group by anumber, trunc(appDate) having count(*) > 2;
dummy number;
BEGIN
open c_cnt;
fetch c_cnt in dummy;
if c_cnt%found then
close c_cnt;
RAISE_APPLICATION_ERROR(-20001, 'error msg');
end if;
close c_cnt;
END;
Obviously, querying the whole table will be inefficient at scale. (One of the reasons why triggers are not recommended for this sort of thing). So this is a situation in which we might want to use a compound trigger (assuming we're on 11g or later).

Oracle SP2-0552: Bind variable "NEW" is not declared

I am trying to create a simple trigger but I got below error. I searched on the internet but could not find the solution. Could you help me on this issue?
create trigger ProcessTigger before insert on T039
for each row
declare consecutivo int; idconsecutivo int; maxconsecutivo int;
begin
select t326c004 into consecutivo from T326 where t326c003 = 'T039' and t326c002 = :new.t039c004;
if consecutivo is not null
then
consecutivo :=consecutivo+1;
select t326c001 into idconsecutivo from T326 where t326c002 = :new.t039c004 and t326c003=T039;
update T326 set t326c004 = consecutivo where t326c001=idconsecutivo and t326c003=T039;
else
select max(t039c003) into maxconsecutivo from T039 where t071c002=:new.t039c004;
if maxconsecutivo is not null
then consecutivo := maxconsecutivo+1;
else consecutivo:=1;
end if;
insert into T326
(t326c002,t326c003,t326c004)values(:new.t039c004,'T039',consecutivo);
end if;
end;
ERROR:
SP2-0552: Bind variable "NEW" is not declared.
If this is your idea of "a simple trigger" then I wonder what a complicated one would like?
It seems likely that the SP2-0552 error is because you're running a script with rogue newlines without setting SQLBLANKLINES.
But once you've fixed the syntax errors you'll find your trigger won't run due to a mutating table error. We can't select in a trigger from the underlying table because the state is indeterminate. So this is wrong:
select max(t039c003) into maxconsecutivo
from T039
where t071c002=:new.t039c004;
You need to find a different way of implementing whatever business rule that is supposed to do.
The use of triggers for such function of dispensing IDs is not safe. Remember there could be more than an insert that will race to get the next 'consecutivo' and get the same ID
Also, the issue of mutating table, where you cannot select from the same table in a row-level trigger.
In addition to that, you have syntax error in lines like the below where you're not enclosing T039 with quotes!
select t326c001 into idconsecutivo from T326 where t326c002 = :new.t039c004
and t326c003=T039;
update T326 set t326c004 = consecutivo where t326c001=idconsecutivo and
t326c003=T039;
I suspect the error that you get is due to an invalid reference to a column (when using :new)
You can try the following trigger and function:
Create an autonomous_transaction function to insert the initial "consecutivo"
In the trigger, start with insert (calling the function), and if not creating a record, then update
create or replace
trigger processtrigger
before insert on t039
for each row
declare
v_id number;
begin
-- start with insert calling the function
if f_create_new_con(:new.t039c004) = 0 then
update t326 set t326c004 = t326c004 + 1 -- this is safe since it will take the last committed t326c004 and increase it
where t326c003 = 'T039'
and t326c002 = :new.t039c004;
end if;
end;
/
create or replace
function f_create_new_con(p_value in number) return number
is
pragma autonomous_transaction;
begin
insert into t326 (t326c002, t326c003, t326c004)
select p_value, 'T039', (select nvl(max(t039c003), 0) + 1 from t039 where t071c002 = p_value)
from dual
where not exists (select 1 from t326 where t326c002 = p_value and t326c003 = 'T039');
-- if no insert then return 0 to update
if (sql%rowcount = 0) then
return 0;
else
return 1;
end if;
end;
/

Create 'TABLE AS' in PL/SQL block

My query is simple but PL/SQL code block is expecting 'INTO' statement.
Here is my query:
DECLARE
yesterdays_date DATE := SYSDATE-1;
start_date DATE :='01/JAN/2013';
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE P2P_DATA';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -942 THEN
--if table not found DO NOTHING AND MOVE ON
--dbms_output.put_line('HELLO');
NULL;
END IF;
---------------------------create new table here-------------------
CREATE TABLE P2P_DATA AS
SELECT
GM_NAME,
NEW_SKILL,
WEEK_DATE,
TOR_MWF
FROM TEST_TABLE
WHERE WEEK_DATE BETWEEN start_date AND yesterdays_date;
END;
it gives the COMPILE time error:
FOUND CREATE: EXPECTING END SELECT or (BEGIN CASE CLOSE CONTINUE DECLARE ... ETC)
I am simply setting the dates in the declaration block and then creating a new table as a result of the select statement. any ideas how to accomplish this task?
This does the same thing but avoids the execute immediate and overhead of creating and dropping tables.
DECLARE
yesterdays_date DATE := SYSDATE-1;
start_date DATE :='01/JAN/2013';
BEGIN
--clear out old data------
DELETE from P2P_DATA;
---------------------------insert new data here-----------------
INSERT INTO P2P_DATA
SELECT
GM_NAME,
CASE WHEN SUBSTR(CST_NAME,0,5) = 'A'
THEN 'BVG1' ELSE SUBSTR(CST_NAME,0,5) END AS CST_NAME,
NEW_SKILL,
WEEK_DATE,
TOR_MWF,
TOR_MA,
TOR_DL
FROM TEST_TABLE
WHERE WEEK_DATE BETWEEN start_date AND yesterdays_date;
END;
--do your commit outside the transaction just in case
Even better just create a view and avoid the whole table thing when all you want is a subset.
CREATE VIEW VW_P2P_DATA
AS
SELECT
GM_NAME,
CASE WHEN SUBSTR(CST_NAME,0,5) = 'A'
THEN 'BVG1' ELSE SUBSTR(CST_NAME,0,5) END AS CST_NAME,
NEW_SKILL,
WEEK_DATE,
TOR_MWF,
TOR_MA,
TOR_DL
FROM TEST_TABLE
WHERE WEEK_DATE BETWEEN TO_DATE('01/JAN/2013','DD/MON/YYYY') AND SYSDATE-1;
You can not do DDL queries within a PL/SQL block; if you need to do so, you have to use dinamic SQL; for example, you could use:
execute immediate '
CREATE TABLE P2P_DATA AS
SELECT
GM_NAME,
NEW_SKILL,
WEEK_DATE,
TOR_MWF
FROM TEST_TABLE
WHERE WEEK_DATE BETWEEN start_date AND yesterdays_date;
';
However, please consider solutions different from create/drop tables on the fly if you can
Actually, I'll put this as an answer instead of a comment ...
I'd recommend avoiding the PL/SQL entirely ... just do the following:
truncate table P2P_DATA;
insert into P2P_DATA
SELECT
GM_NAME,
NEW_SKILL,
WEEK_DATE,
TOR_MWF
FROM TEST_TABLE
WHERE WEEK_DATE BETWEEN to_date('01/JAN/2013','dd/MON/yyyy') AND SYSDATE-1;
Simpler, cleaner, and faster. ;)

trigger after insert on table

create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
declare
nmarcacaoa number;
ncartaoa number;
begin
select nmarcacao into nmarcacaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
select ncartao into ncartaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
insert_pagamentos(nmarcacaoa, ncartaoa); --this is a procedure
exception when others then
raise_application_error(-20001, 'Error in Trigger!!!');
end addpagamento;
when i try to run the insert statement to the table "marcacoes_refeicoes" this procedure gives error: like the table is mutating
create or replace
procedure insert_pagamentos
(nmarcacaoa in number, ncartaoa in number)
AS
BEGIN
insert into pagamentos (nmarcacao, datapagamento, ncartao) values (nmarcacaoa, sysdate, ncartaoa);
commit;
END INSERT_PAGAMENTOS;
Short (oversimplified) answer:
You can't modify a table in a trigger that changes the table.
Long answer:
http://www.oracle-base.com/articles/9i/mutating-table-exceptions.php has a more in-depth explanation, including suggestions how to work around the problem.
You're hitting the mutating-table problem because you're selecting from the same table the trigger is on, but what you seem to be trying to do doesn't make sense. Your queries to get a value for nmarcacaoa and ncartaoa will return a no_data_found or too_many_rows error unless the table had exactly 2 rows in it before your insert:
select nmarcacao from marcacoes_refeicoes
where rownum < (select count(*) from marcacoes_refeicoes);
will return all rows except one; and the one that's excluded will be kind of random as you have no ordering. Though the state is undetermined within the trigger, so you can't really how many rows there are, and it won't let you do this query anyway. You won't normally be able to get a single value, anyway, and it's not obvious which value you actually want.
I can only imagine that you're trying to use the values from the row you are currently inserting, and putting them in your separate payments (pagamentos) table. If so there is a built-in mechanism to do that using correlation names, which lets you refer to the newly inserted row as :new (by default):
create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
begin
insert_pagamentos(:new.nmarcacaoa, :new.ncartaoa);
end addpagamento;
The :new is referring to the current row, so :new.nmarcacaoa is the nmarcacaoa being inserted. You don't need to (and can't) get that value from the table itself. (Even with the suggested autonomous pragma, that would be a separate transaction and would not be able to see your newly inserted and uncommitted data).
Or you can just do the insert directly; not sure what the procedure is adding here really:
create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
begin
insert into pagamentos(nmarcacao, datapagamento, ncartao)
values (:new.nmarcacaoa, sysdate, :new.ncartaoa);
end addpagamento;
I've removed the exception handler as all it was doing was masking the real error, which is rather unhelpful.
Editing this answer in view of the comments below:
You can use PRAGMA AUTONOMOUS_TRANSACTION to get rid of the error, but DONOT USE it as it will NOT solve any purpose.
Use PRAGMA AUTONOMOUS_TRANSACTION:
create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
declare
PRAGMA AUTONOMOUS_TRANSACTION;
nmarcacaoa number;
ncartaoa number;
begin
select nmarcacao into nmarcacaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
select ncartao into ncartaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
insert_pagamentos(nmarcacaoa, ncartaoa); --this is a procedure
exception when others then
raise_application_error(-20001, 'Error in Trigger!!!');
end addpagamento;
However in this case , select count(*) from marcacoes_refeicoes will give you the new count after the current insertion into the table.

Resources