Trigger — is invalid and failed re-validation - sql

I am trying to create this simple trigger :
CREATE OR REPLACE TRIGGER my_trig
BEFORE DELETE OR INSERT OR UPDATE ON empcopy
FOR EACH ROW
WHEN (NEW.EID > 0)
DECLARE
sal_diff number;
BEGIN
sal_diff := :NEW.salary - :OLD.salary;
dbms_output.put_line('Old salary: ' || :OLD.salary);
dbms_output.put_line('New salary: ' || :NEW.salary);
dbms_output.put_line('Salary difference: ' || sal_diff);
COMMIT;
END;
/
on execution it gives the result as:
Trigger created
But when I update my table the following result is obtained:
update empcopy
set salary=salary+5000;
after execution:
error at line 1
ORA-0498:triiger 'HR.MY_TRIG' is invalid and failed re-validation.

You can't COMMIT without using a PRAGMA AUTONOMOUS_TRANSACTION in your DECLARE block.
If the database allowed committing in row-level triggers, this could enable committing part of the rows in a statement before the others had been evaluated and would disrupt the statement as a unit--all rows in an individual statement should complete their change and be committed together.
If you were to use an AUTONOMOUS_TRANSACTION in this trigger, this would allow the trigger to execute new UPDATEs, DELETEs, etc in a transaction independent from other active DML changes in the open transaction.
But note, in you're case, your TRIGGER isn't actually executing any mutational DML, nor even any reads, so you don't need a COMMIT at all. All you need to do is drop your COMMIT as below.
CREATE OR REPLACE TRIGGER my_trig
BEFORE DELETE OR INSERT OR UPDATE ON empcopy
FOR EACH ROW
WHEN (NEW.EID > 0)
DECLARE
sal_diff number;
BEGIN
sal_diff := :NEW.salary - :OLD.salary;
dbms_output.put_line('Old salary: ' || :OLD.salary);
dbms_output.put_line('New salary: ' || :NEW.salary);
dbms_output.put_line('Salary difference: ' || sal_diff);
END;
/
I would however suggest a couple other changes as well.
As other triggers can also change the NEW value, you might consider making this an AFTER TRIGGER, to only log the final state.
Also this trigger will not log any DELETEs, since DELETEs will all have a NULL :NEW.EID. I'd suggest either dropping the AFTER DELETE if logging DELETEs is not intended, or handling DELETEs separately via a CASE WHEN DELETING statement.
CREATE OR REPLACE TRIGGER MY_TRIG
AFTER DELETE OR INSERT OR UPDATE ON EMPCOPY
FOR EACH ROW
DECLARE
SAL_DIFF NUMBER;
BEGIN
CASE WHEN DELETING
THEN
DBMS_OUTPUT.put_line('Log the delete here if you want.');
WHEN (:NEW.EID > 0)
THEN
SAL_DIFF := COALESCE(:NEW.SALARY, 0) - COALESCE(:OLD.SALARY, 0);
DBMS_OUTPUT.put_line('Old salary: ' || :OLD.SALARY);
DBMS_OUTPUT.put_line('New salary: ' || :NEW.SALARY);
DBMS_OUTPUT.put_line('Salary difference: ' || SAL_DIFF);
ELSE NULL;
END CASE;
END;
/
Also DBMS_OUTPUT is transient logging. In case you want to keep a record of changes to EMPCOPY more permanently, Oracle has tools available to automate and control change-tracking in your data, such as audit trail and FGA.
EDIT: examples below.
Create a test table:
CREATE TABLE EMPCOPY(
EID NUMBER NOT NULL,
SALARY NUMBER
);
Table EMPCOPY created.
Then create the trigger:
CREATE OR REPLACE TRIGGER MY_TRIG
AFTER DELETE OR INSERT OR UPDATE ON EMPCOPY
FOR EACH ROW
DECLARE
SAL_DIFF NUMBER;
BEGIN
CASE WHEN DELETING
THEN
DBMS_OUTPUT.put_line('Log the delete here if you want.');
WHEN (:NEW.EID > 0)
THEN
SAL_DIFF := COALESCE(:NEW.SALARY, 0) - COALESCE(:OLD.SALARY, 0);
DBMS_OUTPUT.put_line('Old salary: ' || :OLD.SALARY);
DBMS_OUTPUT.put_line('New salary: ' || :NEW.SALARY);
DBMS_OUTPUT.put_line('Salary difference: ' || SAL_DIFF);
ELSE NULL;
END CASE;
END;
/
Trigger MY_TRIG compiled
Then test it:
SQL> --Should not log, EMPID is not greater than zero.
SQL> INSERT INTO EMPCOPY VALUES (-13, 50000);
1 row inserted.
SQL> --Should log, EMPID is greater than zero.
SQL> INSERT INTO EMPCOPY VALUES (1919, 75000);
Old salary:
New salary: 75000
Salary difference: 75000
1 row inserted.
SQL> -- The statement you provided. This should log for EMPID=1919 but not EMPID=-13
SQL> update empcopy
2 set salary=salary+5000;
Old salary: 75000
New salary: 80000
Salary difference: 5000
2 rows updated.
SQL> -- This should log a PLACEHOLDER value for each row on delete.
SQL> DELETE FROM EMPCOPY;
Log the delete here if you want.
Log the delete here if you want.
2 rows deleted.

Related

Implicit cursor indicates that it updates all rows in the table for a given identifier

I have a simple PL/SQL procedure that increases the salary of an employee in the EMP table of the SCOTT schema. This receives the employee number per parameter and increment. The UPDATE statement that performs the update does not filter by that identifier and when accessing the ROWCOUNT of the cursor indicates that all the rows in the table were updated.
If this update I do from SQL Plus. It only updates a row.
CREATE OR REPLACE PROCEDURE INCREASE_SALARY(
empno EMP.EMPNO%TYPE,
incre NUMBER
)
AUTHID DEFINER
AS PRAGMA AUTONOMOUS_TRANSACTION;
INCREMENT_MUST_BE_GREATER_THAN_ZERO EXCEPTION;
BEGIN
IF incre = 0 THEN
RAISE INCREMENT_MUST_BE_GREATER_THAN_ZERO;
END IF;
DBMS_OUTPUT.PUT_LINE('EMPLOYEE TO UPDATE: ' || empno);
UPDATE EMP
SET SAL = SAL + (SAL * incre / 100)
WHERE EMPNO = empno;
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQL%ROWCOUNT)||' rows affected.');
IF SQL%ROWCOUNT > 0 THEN
INSERT INTO TABLA_LOG VALUES(USER, SYSDATE);
DBMS_OUTPUT.PUT_LINE('SALARY UPDATED SUCCESSFULLY');
ELSE
DBMS_OUTPUT.PUT_LINE('NO EMPLOYEE FOUND FOR THAT ID');
END IF;
COMMIT WORK;
EXCEPTION
WHEN INCREMENT_MUST_BE_GREATER_THAN_ZERO THEN
DBMS_OUTPUT.PUT_LINE('THE INCREMENT PERCENTAGE MUST BE GREATER THAN 0');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('ERROR : ' || SQLCODE || 'MENSAJE: ' || SQLERRM);
END;
/
When executing the procedure with the EMPNO 7900 (JAMES)
SET SERVEROUTPUT ON;
EXEC INCREASE_SALARY(7900, 20);
I get the following output:
EMPLOYEE TO UPDATE: 7900
13 rows affected.
SALARY UPDATED SUCCESSFULLY
Procedimiento PL/SQL terminado correctamente.
Does anyone know I'm doing wrong? Thanks in advance.
select count(1) from EMP WHERE EMPNO = 7900;
Return 1.
I'm using Oracle Enterprise 12c. The procedure compiles. The identifier error too long does not appear.
Your problem is empno = empno. These are interpreted as the column name.
You should try to give your arguments and local variables different names:
CREATE OR REPLACE PROCEDURE INCREASE_SALARY (
in_empno EMP.EMPNO%TYPE,
iin_ncre NUMBER
)
AUTHID DEFINER
AS PRAGMA AUTONOMOUS_TRANSACTION;
INCREMENT_MUST_BE_GREATER_THAN_ZERO EXCEPTION;
BEGIN
IF in_incre = 0 THEN
RAISE INCREMENT_MUST_BE_GREATER_THAN_ZERO;
END IF;
DBMS_OUTPUT.PUT_LINE('EMPLOYEE TO UPDATE: ' || in_empno);
UPDATE EMP
SET SAL = SAL + (SAL * in_incre / 100)
WHERE EMPNO = in_empno;
. . .

SP2-0552: Bind Variable "NEW" is not declared

I am trying to learn pl/sql triggers. I am trying to create a simple trigger by tracking tutorial http://www.tutorialspoint.com/plsql/plsql_triggers.htm but I got below error. I searched on the internet but could not find the solution. Could you help me on this issue?
CREATE OR replace TRIGGER display_salary_changes
BEFORE DELETE OR INSERT OR UPDATE ON ok.customers
FOR EACH ROW
DECLARE
sal_diff NUMBER;
BEGIN
sal_diff := :NEW.salary - :OLD.salary;
END;
/
Trıgger DISPLAY_SALARY_CHANGES created.
SP2-0552: Bind Variable "NEW" is not declared.
PL/SQL procedure successfully completed.
Edit: I am using Sql Developer Version 4.1.1
Works for me (example from your link, but it's basically the same as your post):
SQL> create table demo (id integer, salary number);
Table created.
SQL> create or replace trigger display_salary_changes
2 before delete or insert or update on demo
3 for each row
4 when (new.id > 0)
5 declare
6 sal_diff number;
7 begin
8 sal_diff := :new.salary - :old.salary;
9 dbms_output.put_line('Old salary: ' || :old.salary);
10 dbms_output.put_line('New salary: ' || :new.salary);
11 dbms_output.put_line('Salary difference: ' || sal_diff);
12 end;
13 /
Trigger created.
SQL> show errors
No errors.
SQL> insert into demo (id, salary) values (1, 100);
Old salary:
New salary: 100
Salary difference:
1 row created.
SQL> update demo set salary = salary * 1.1 where id = 1;
Old salary: 100
New salary: 110
Salary difference: 10
1 row updated.
In your example it shows
Trıgger DISPLAY_SALARY_CHANGES created.
which doesn't look like SQL*Plus output. What tool did you use?
After that it gives a SQL*Plus SP2-0552 error about an undefined bind variable, followed by
PL/SQL procedure successfully completed.
What procedure was that? I suspect this is the output from a script with some other step that is failing after the trigger is created.
Is the trigger valid? You can normally right-click and check properties in desktop tools, or at the SQL*Plus prompt enter
show errors trigger display_salary_changes
Try this:
CREATE OR replace TRIGGER test_trg
BEFORE DELETE OR INSERT OR UPDATE ON test
FOR EACH ROW
DECLARE
sal_diff NUMBER;
BEGIN
sal_diff := :new.d - :old.d;
END;
/
Can you please check you column name. I have tried your code below and I got output.
create table test
(
no number(10),
sal number(10)
);
CREATE OR replace TRIGGER test_tr
BEFORE DELETE OR INSERT OR UPDATE ON test
FOR EACH ROW
DECLARE
sal_diff NUMBER;
BEGIN
sal_diff := :NEW.sal - :OLD.sal;
dbms_output.put_line(sal_diff);
END;
/
insert into test values(1,100);
update test set sal=200 where no=1;
Output :
1 rows inserted.
4 rows updated.
100
100
100
100
1 rows inserted.
I think it is missing the "REFERENCING NEW AS NEW OLD AS OLD" sentence:
CREATE TRIGGER [trigger_name]
BEFORE INSERT OR UPDATE ON [table_name]
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW

SQL trigger to show salary change

I am trying to create a simple trigger in sql developer to display the change in salary when it is changed
CREATE OR REPLACE TRIGGER salary_changes
BEFORE DELETE OR INSERT OR UPDATE ON FACULTY
FOR EACH ROW
DECLARE
sal_diff NUMBER;
BEGIN
sal_diff := :NEW.F_SALARY - :OLD.F_SALARY;
DBMS_OUTPUT.PUT_LINE('Difference: ' || sal_diff);
END;
when I attempt to run the trigger it prompts be to enter binds for NEW and OLD and when i try run an update to see if it works it states the trigger failed. So how am i using the old and new tags incorrectly? or is that not the issue
There are several issues with your code.
You need to create a after trigger instead of the before trigger.
You are trying to write a trigger that performs an operation for
insert,delete or update. So you should do the conditional checks
(such as if inserting,deleting or updating) clause.
Also, when you do delete, there is no new value, but just the old
value.
I would change your trigger as below..
CREATE OR REPLACE TRIGGER salary_changes
AFTER DELETE OR INSERT OR UPDATE ON FACULTY
FOR EACH ROW
DECLARE
sal_diff NUMBER;
BEGIN
If (INSERTING or UPDATING) then
sal_diff := :NEW.F_SALARY - :OLD.F_SALARY;
DBMS_OUTPUT.PUT_LINE('Difference: ' || sal_diff);
END IF;
IF DELETING THEN
DBMS_OUTPUT.PUT_LINE('The deleted value is:' || :OLD.F_SALARY);
END IF;
END;

SQL Trigger time check + - 1hour [duplicate]

I am writing a simple trigger that is supposed to just send a message with the updated Count of rows as well as the old value of Gender and the updated value of Gender. When i run an update however I am getting the error that the table is mutating and the table might not be able to see it but I'm not exactly sure why.
trigger
create or replace trigger updatePERSONS
after update
on PERSONS
for each row
declare
n int;
oldGender varchar(20):= :OLD.Gender;
newGender varchar(20):= :NEW.Gender;
begin
select Count(*)
into n
from PERSONS;
if (oldGender != newGender) then
dbms_output.put_line('There are now '|| n || ' rows after update. Old gender: ' || oldGender
|| ', new Gender: ' || newGender);
end if;
End;
`
i know it has to do with the select statement after begin but how else would i get count of rows?
As #San points out, a row-level trigger on persons cannot generally query the persons table.
You'd need two triggers, a row-level trigger that can see the old and new gender and a statement-level trigger that can do the count. You could also, if you're using 11g, create a compound trigger with both row- and statement-level blocks.
create or replace trigger trg_stmt
after update
on persons
declare
l_cnt integer;
begin
select count(*)
into l_cnt
from persons;
dbms_output.put_line( 'There are now ' || l_cnt || ' rows.' );
end;
create or replace trigger trg_row
after update
on persons
for each row
begin
if( :new.gender != :old.gender )
then
dbms_output.put_line( 'Old gender = ' || :old.gender || ', new gender = ' || :new.gender );
end if;
end;

Best way to reset an Oracle sequence to the next value in an existing column?

For some reason, people in the past have inserted data without using sequence.NEXTVAL. So when I go to use sequence.NEXTVAL in order to populate a table, I get a PK violation, since that number is already in use in the table.
How can I update the next value so that it is usable? Right now, I'm just inserting over and over until it's successful (INSERT INTO tbl (pk) VALUES (sequence.NEXTVAL)), and that syncs up the nextval.
You can temporarily increase the cache size and do one dummy select and then reset the cache size back to 1. So for example
ALTER SEQUENCE mysequence INCREMENT BY 100;
select mysequence.nextval from dual;
ALTER SEQUENCE mysequence INCREMENT BY 1;
In my case I have a sequence called PS_LOG_SEQ which had a LAST_NUMBER = 3920.
I then imported some data from PROD to my local machine and inserted into the PS_LOG table. Production data had more than 20000 rows with the latest LOG_ID (primary key) being 20070. After importing I tried to insert new rows in this table but when saving I got an exception like this one:
ORA-00001: unique constraint (LOG.PS_LOG_PK) violated
Surely this has to do with the Sequence PS_LOG_SEQ associated with the PS_LOG table. The LAST_NUMBER was colliding with data I imported which had already used the next ID value from the PS_LOG_SEQ.
To solve that I used this command to update the sequence to the latest \ max(LOG_ID) + 1:
alter sequence PS_LOG_SEQ restart start with 20071;
This command reset the LAST_NUMBER value and I could then insert new rows into the table. No more collision. :)
Note: this alter sequence command is new in Oracle 12c.
Note: this blog post documents the ALTER SEQUENCE RESTART option does exist, but as of 18c, is not documented; it is apparently intended for internal Oracle use.
These two procedures let me reset the sequence and reset the sequence based on data in a table (apologies for the coding conventions used by this client):
CREATE OR REPLACE PROCEDURE SET_SEQ_TO(p_name IN VARCHAR2, p_val IN NUMBER)
AS
l_num NUMBER;
BEGIN
EXECUTE IMMEDIATE 'select ' || p_name || '.nextval from dual' INTO l_num;
-- Added check for 0 to avoid "ORA-04002: INCREMENT must be a non-zero integer"
IF (p_val - l_num - 1) != 0
THEN
EXECUTE IMMEDIATE 'alter sequence ' || p_name || ' increment by ' || (p_val - l_num - 1) || ' minvalue 0';
END IF;
EXECUTE IMMEDIATE 'select ' || p_name || '.nextval from dual' INTO l_num;
EXECUTE IMMEDIATE 'alter sequence ' || p_name || ' increment by 1 ';
DBMS_OUTPUT.put_line('Sequence ' || p_name || ' is now at ' || p_val);
END;
CREATE OR REPLACE PROCEDURE SET_SEQ_TO_DATA(seq_name IN VARCHAR2, table_name IN VARCHAR2, col_name IN VARCHAR2)
AS
nextnum NUMBER;
BEGIN
EXECUTE IMMEDIATE 'SELECT MAX(' || col_name || ') + 1 AS n FROM ' || table_name INTO nextnum;
SET_SEQ_TO(seq_name, nextnum);
END;
If you can count on having a period of time where the table is in a stable state with no new inserts going on, this should do it (untested):
DECLARE
last_used NUMBER;
curr_seq NUMBER;
BEGIN
SELECT MAX(pk_val) INTO last_used FROM your_table;
LOOP
SELECT your_seq.NEXTVAL INTO curr_seq FROM dual;
IF curr_seq >= last_used THEN EXIT;
END IF;
END LOOP;
END;
This enables you to get the sequence back in sync with the table, without dropping/recreating/re-granting the sequence. It also uses no DDL, so no implicit commits are performed. Of course, you're going to have to hunt down and slap the folks who insist on not using the sequence to populate the column...
With oracle 10.2g:
select level, sequence.NEXTVAL
from dual
connect by level <= (select max(pk) from tbl);
will set the current sequence value to the max(pk) of your table (i.e. the next call to NEXTVAL will give you the right result); if you use Toad, press F5 to run the statement, not F9, which pages the output (thus stopping the increment after, usually, 500 rows).
Good side: this solution is only DML, not DDL. Only SQL and no PL-SQL.
Bad side : this solution prints max(pk) rows of output, i.e. is usually slower than the ALTER SEQUENCE solution.
Today, in Oracle 12c or newer, you probably have the column defined as GENERATED ... AS IDENTITY, and Oracle takes care of the sequence itself.
You can use an ALTER TABLE Statement to modify "START WITH" of the identity.
ALTER TABLE tbl MODIFY ("ID" NUMBER(13,0) GENERATED BY DEFAULT ON NULL AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 3580 NOT NULL ENABLE);
In my case I used an approach to reset sequence to zero and than setting from zero to max of target table:
DECLARE
last_val NUMBER;
next_val NUMBER;
BEGIN
SELECT MAX(id_field) INTO next_val FROM some_table;
IF next_val > 0 THEN
SELECT some_table_seq.nextval INTO last_val FROM DUAL;
EXECUTE IMMEDIATE 'ALTER SEQUENCE some_table_seq INCREMENT BY -' || last_val || ' MINVALUE 0';
SELECT some_table_seq.nextval INTO last_val FROM DUAL;
EXECUTE IMMEDIATE 'ALTER SEQUENCE some_table_seq INCREMENT BY ' || next_val;
SELECT some_table_seq.nextval INTO last_val FROM DUAL;
EXECUTE IMMEDIATE 'ALTER SEQUENCE some_table_seq INCREMENT BY 1 MINVALUE 1';
END IF;
END;
Apologies for not having a one-liner solution, since my program runs in Typeorm with node-oracle. However, I think the following SQL commands would help with this problem.
Get the LAST_NUMBER from your sequence.
SELECT SEQUENCE_NAME, LAST_NUMBER FROM ALL_SEQUENCES WHERE SEQUENCE_NAME = '${sequenceName}'
Get the value of your PK from the last row (in this case ID is the PK).
SELECT ID FROM ${tableName} ORDER BY ID DESC FETCH NEXT 1 ROWS ONLY
Lastly update LAST_NUMBER to the value + 1:
ALTER SEQUENCE ${sequenceName} RESTART START WITH ${value + 1}