Automatically update a column in a table with a trigger - sql

I'm having a problem with a trigger code (table mutation and more!) and I can't find
what is the problem.
Basically, I have a table SEMESTER(id_semester, semester_name, begin_date, end_date).
On the insertion of a row, I want the semester_name to be updated with a value according
to what's in begin_date. For example, if the begin_date is '2000-01-01', I want the value of
semester_name to be W00 (for winter 2000).
My first try was to write an 'after insert' trigger, which didn't work because of a table mutation error. Here it is:
CREATE TRIGGER Test
BEFORE INSERT ON Semester
FOR EACH ROW
DECLARE
sem CHAR(1);
year CHAR(2);
BEGIN
-- begin_date is either 1, 5 or 9.
IF (EXTRACT(MONTH FROM :new.begin_date) = '1') THEN
saison := 'W';
ELSIF (EXTRACT(MONTH FROM :new.begin_date) = '5') THEN
saison := 'S';
ELSE
saison := 'F';
END IF;
year := TO_CHAR(:new.date_debut, 'MM');
UPDATE Semester
SET semester_name = CONCAT(sem, year)
WHERE id_semester = :new.id_semester;
END;
/
After, I tried to make a 'before insert' trigger, thinking it would work better but it does not.
Anyone could point me in the right direction?
Thanks!

Assuming id_semester is the primary key, instead of an UPDATE statement, you would just want to assign the :new.semester_name
:new.semester_name := concat( sem, year );

The mutanting table error occurs only with "each row" kind of triggers, try to change your after insert trigger for a "statement" type

Related

Compare Date with Trigger on Oracle SQL

I have a table with those columns:
DateDebVal DATE
DateFinVal DATE
isCurrent NUMBER
And I need to have a trigger that do something like this:
CREATE TRIGGER check_date
AFTER INSERT OR UPDATE ON tablename
FOR EACH ROW
BEGIN
IF sysdate < DateFinVal
set isCurrent = 1
ELSE
set isCurrent = 0
END;
Can I compare those dates? Is it a good idea to use a trigger instead of a view or a procedure?
Dates can absolutely be compared, but assignment is done using := in PL/SQL (not with set)
If you want to change the value of a newly inserted row, you have to use a BEFORE trigger:
CREATE TRIGGER check_date
BEFORE INSERT OR UPDATE ON tablename
FOR EACH ROW
BEGIN
IF sysdate < :new.datefinval
:new.iscurrent := 1;
ELSE
:new.iscurrent := 0;
END IF;
END;
/
Online example: https://dbfiddle.uk/?rdbms=oracle_11.2&fiddle=796347f1b5811448dddf2d0a532c2c2c
Sysdate and DateFinVal are of DATE type so you would compare those. It depends what for you need this trigger.

PL/SQL Trigger activation on specific months

i'm a university student working on a simple database project for our DB exam. I've reached the stage of developing the dynamic constraint for the DB via PL/SQL trigger; the database applies to a oil/gas refinirey enviroment.
Here is table on which the trigger should work:
CREATE TABLE FEED(
NUMBER_STOCK NUMBER(2) CHECK(NUMBER_STOCK>0),
REACT_NAME VARCHAR(20),
PROD_LANE_NUMB NUMBER(2),
DATE_FEED DATE PRIMARY KEY,
QUANTITY NUMBER(5) NOT NULL CHECK (QUANTITY>0),
CONSTRAINT FKA FOREIGN KEY (NUMBER_STOCK) REFERENCES STOCKS(NUMBER_STOCK) ON DELETE CASCADE,
CONSTRAINT FKA2 FOREIGN KEY (REACT_NAME,PROD_LANE_NUMB) REFERENCES PRODUCTION_PLANTS(REACT_NAME,PROD_LANE_NUMB) ON DELETE CASCADE
);
The trigger i am trying to develop has the following purpose:
During the winter and summer months(December-February and June-August) the production plants cannot work at full load due to many weather related factors, such corrosion, pipe dilation/constriction and higher/lower temepratures. During these months the QUANTITY of raw materials sent daily to the plants, must be less than the average of total QUANTITY sent in the months preeciding those periods( November and May).
Now here's how i developed my trigger(Note: there is already another AFTER INSERT OR UPDATE trigger active on the table):
CREATE OR REPLACE TRIGGER METEO
AFTER INSERT OR UPDATE ON FEED
FOR EACH ROW
DECLARE
ACTL_MONTH CHAR(3); --ACTUAL MONTH WITHIN 3 LETTER FORMAT --
MONTH_AVG NUMBER(8) := 0; --PREECIDING MONTHS AVARAGE--
FEED_QUAN NUMBER(8) := 0; --ACTUAL FEED INSERTED/UPDATED--
BEGIN
--GETTING DATE FROM DUAL IN 3 LETTER FORMAT--
SELECT TO_CHAR(TRUNC(SYSDATE,'MONTH'),'MON') INTO ACTL_MONTH FROM DUAL;
--CHECKING DATE--
IF ACTL_MONTH='MAY' THEN
SELECT AVG(QUANTITY) INTO MONTH_AVG FROM FEED WHERE TO_CHAR(TRUNC(DATE_FEED,'MONTH'),'MON')='MAY';
END IF;
IF ACTL_MONTH='NOV' THEN
SELECT AVG(QUANTITY) INTO MONTH_AVG FROM FEED WHERE TO_CHAR(TRUNC(DATE_FEED,'MONTH'),'MON')='NOV';
END IF;
--SELECTING THE QUANTITY FEEDED--
SELECT :NEW.QUANTITY INTO FEED_QUAN FROM FEED;
IF MONTH_AVG<FEED_QUAN THEN
RAISE_APPLICATION_ERROR(-20008,'EXCEEDING FEED QUANTITY DUE TO WEATHER LIMITATIONS.ROLLBACK');
END IF;
END;
/
But every time i insert a value the trigger does not fire, and allows me to insert/update rows with not allowed values.
So:
Did i made mistakes in PL/SQL code?(It compiled with no errors)
May trigger cannot be fired upon dates?
Should i use another type of trigger structure?(statement?before?)
This is my first question on stack overflow so have mercy on me, and yes i have used google and used stack search for similar question but did not find anything like my problem, if something is not clear point it to me and i will adjust/explain; besisde consdier that im a foreign student so expect LOTS of english grammar errors.
There are a couple of different ways to solve this problem. First, create a second table to hold your monthly averages:
CREATE TABLE MONTHLY_AVERAGE_FEED
(MONTH_YEAR DATE -- 01-MM-YYYY
PRIMARY KEY,
TOTAL_QUANTITY NUMBER,
TRANSACTIONS NUMBER,
AVERAGE_QUANTITY NUMBER
GENERATED ALWAYS AS (CASE
WHEN TRANSACTIONS > 0 THEN
TOTAL_QUANTITY / TRANSACTIONS
ELSE 0
END));
and maintained by a trigger:
CREATE TRIGGER FEED_AVERAGE_AIUD
AFTER INSERT OR UPDATE OR DELETE ON FEED
FOR EACH ROW
BEGIN
IF UPDATING OR DELETING THEN
-- Back out the 'old' values
UPDATE MONTHLY_AVERAGE_FEED
SET TOTAL_QUANTITY = GREATEST(TOTAL_QUANTITY - :OLD.QUANTITY, 0),
TRANSACTIONS = GREATEST(TRANSACTIONS - 1, 0)
WHERE MONTH_YEAR = TRUNC(:OLD.DATE_FEED, 'MONTH');
END IF;
IF INSERTING OR UPDATING THEN
MERGE INTO MONTHLY_AVERAGE_FEED maf
USING (SELECT TRUNC(:NEW.DATE_FEED, 'MONTH') AS MONTH_YEAR
FROM DUAL) d
ON (maf.MONTH_YEAR = d.MONTH_YEAR)
WHEN MATCHED THEN
UPDATE
SET TOTAL_QUANTITY := TOTAL_QUANTITY +
(:NEW.QUANTITY *
CASE
WHEN INSERTING OR UPDATING THEN 1
ELSE -1
END),
TRANSACTIONS = TRANSACTIONS + 1
WHEN NOT MATCHED THEN
INSERT (MONTH_YEAR, TRANSACTIONS, TOTAL_QUANTITY)
VALUES (TRUNC(:NEW.DATE_FEED, 'MONTH'), 1, :NEW.QUANTITY);
END IF;
END FEED_AVERAGE_AIUD;
Then in your trigger you can simply query the MONTHLY_AVERAGE_FEED table to get the average feed for whatever month you want.
The second option would be to rewrite #MT0's trigger as a compound trigger. The "BEFORE STATEMENT" section of the trigger would handle computing the average feed for whatever month(s) you want, while the "BEFORE EACH ROW" section would contain the rest of the work.
Thanks to the answers and the help given by Bob Jarvis and MT0 i have finally solved my problem.
So i added a support table for the averages like Bob Jarvis suggested, and then added a specific trigger to populate it when any row is inserted into FEED table of my first post; here's the trigger modified which compile properly:
CREATE OR REPLACE TRIGGER FEED_AVERAGE_AIUD
AFTER INSERT OR UPDATE OR DELETE ON FEED
FOR EACH ROW
BEGIN
IF UPDATING OR DELETING THEN
-- Back out the 'old' values
UPDATE MONTHLY_AVERAGE_FEED
SET TOTAL_QUANTITY = GREATEST(TOTAL_QUANTITY - :OLD.QUANTITY, 0),
TRANSACTIONS = GREATEST(TRANSACTIONS - 1, 0)
WHERE MONTH_YEAR = TRUNC(:OLD.DATE_FEED, 'MONTH');
END IF;
IF INSERTING OR UPDATING THEN
MERGE INTO MONTHLY_AVERAGE_FEED maf
USING (SELECT TRUNC(:NEW.DATE_FEED, 'MONTH') AS MONTH_YEAR
FROM DUAL) d
ON (maf.MONTH_YEAR = d.MONTH_YEAR)
WHEN MATCHED THEN
UPDATE
SET TOTAL_QUANTITY = TOTAL_QUANTITY +:NEW.QUANTITY,
TRANSACTIONS = TRANSACTIONS + 1
WHEN NOT MATCHED THEN
INSERT (MONTH_YEAR, TRANSACTIONS, TOTAL_QUANTITY)
VALUES (TRUNC(:NEW.DATE_FEED, 'MONTH'), 1, :NEW.QUANTITY);
END IF;
END FEED_AVERAGE_AIUD;
/
So with the average table in place and running i created the following trigger to check the consitency beetwen the new inserted/updated values and the averages in specified months:
CREATE OR REPLACE TRIGGER METEO
AFTER INSERT OR UPDATE ON FEED
FOR EACH ROW
DECLARE
ACT_QUANT NUMBER; --ACTUAL INSERTED/UPDATED QUANTITY---
ACT_MONTH NUMBER; --MONTH AT WHICH THE QUANTITY WAS INSERTED/UPDATED--
REF_AVERG NUMBER; --THE AVERAGE IN THE AVERAGES TABLE REFEERING TO THE ISNERTED/UPDATED MONTH--
BEGIN
ACT_MONTH:= EXTRACT(MONTH FROM :NEW.DATE_FEED);
ACT_QUANT:= :NEW.QUANTITY;
-- SO IF I AM INSERTING/UPDATING VALUES IN JUNE/JULY/AUGUST
-- I SIMPLY SEARCH THE AVERAGE TABLE WITH A QUERY WITH MONTH AND YEAR TAKEN
-- BY THE DATE_FEED
IF ACT_MONTH IN(6,7,8) THEN
SELECT AVERAGE_QUANTITY
INTO REF_AVERG
FROM MONTHLY_AVERAGE_FEED
WHERE EXTRACT(MONTH FROM MONTH_YEAR)=5 AND
EXTRACT(YEAR FROM MONTH_YEAR)=EXTRACT(YEAR FROM :NEW.DATE_FEED);
ELSIF ACT_MONTH=12 THEN --FOR DECEMBER I TAKE THE SAME YEAR NOVEMBER AVG--
SELECT AVERAGE_QUANTITY
INTO REF_AVERG
FROM MONTHLY_AVERAGE_FEED
WHERE EXTRACT(MONTH FROM MONTH_YEAR)=11 AND
EXTRACT(YEAR FROM MONTH_YEAR)=EXTRACT(YEAR FROM :NEW.DATE_FEED);
ELSIF ACT_MONTH IN (1,2) THEN --FOR JANUARY AND FEBRUARY I TAKE THE AVG OF THE PREVIOUS YEAR --
SELECT AVERAGE_QUANTITY
INTO REF_AVERG
FROM MONTHLY_AVERAGE_FEED
WHERE EXTRACT(MONTH FROM MONTH_YEAR)=11 AND
EXTRACT(YEAR FROM MONTH_YEAR)=EXTRACT(YEAR FROM :NEW.DATE_ALIM)-1;
END IF;
IF ACT_QUANT>REF_AVERG THEN
RAISE_APPLICATION_ERROR(
-20008,
'EXCEEDING FEED QUANTITY DUE TO WEATHER LIMITATIONS.ROLLBACK'
);
END IF;
END;
/
The trigger compiled and worked as it was intended, it's not elegant for sure, but it does his job. As ever I had to translate every table/attributes names from my original language to english so it's possibile that i forgot something or mispelled word,verbs etc.
Anyway thanks to everyone who replied and helped,hoping this will help somebody else someday, thank you guys.
Change the trigger from AFTER to BEFORE.
You don't need to use SELECT ... INTO to assign variables.
You aren't answering the question. You need to check if the month is Dec-Feb or Jun-Aug and then find the daily average for Nov or May (respectively).
Rather than getting the average for all Mays (or Novembers), you need to get the average for only the preceding May (or November).
Like this:
SET DEFINE OFF;
CREATE OR REPLACE TRIGGER METEO
BEFORE INSERT OR UPDATE ON FEED
FOR EACH ROW
DECLARE
MONTH_START DATE;
MONTH_AVG NUMBER(8);
BEGIN
MONTH_START := CASE EXTRACT( MONTH FROM :NEW.DATE_FEED )
WHEN 12 THEN ADD_MONTHS( TRUNC( :NEW.DATE_FEED, 'MM' ), -1 )
WHEN 1 THEN ADD_MONTHS( TRUNC( :NEW.DATE_FEED, 'MM' ), -2 )
WHEN 2 THEN ADD_MONTHS( TRUNC( :NEW.DATE_FEED, 'MM' ), -3 )
WHEN 6 THEN ADD_MONTHS( TRUNC( :NEW.DATE_FEED, 'MM' ), -1 )
WHEN 7 THEN ADD_MONTHS( TRUNC( :NEW.DATE_FEED, 'MM' ), -2 )
WHEN 8 THEN ADD_MONTHS( TRUNC( :NEW.DATE_FEED, 'MM' ), -3 )
ELSE NULL
END;
IF MONTH_START IS NULL THEN
RETURN;
END IF;
SELECT AVG( QUANTITY )
INTO MONTH_AVG
FROM FEED
WHERE DATE_FEED >= MONTH_START
AND DATE_FEED < ADD_MONTHS( MONTH_START, 1 );
IF MONTH_AVG <= :NEW.QUANTITY THEN
RAISE_APPLICATION_ERROR(-20008,'EXCEEDING FEED QUANTITY DUE TO WEATHER LIMITATIONS.ROLLBACK');
END IF;
END;
/

automatic update of date when inserting new row

I'm trying to create a trigger that will update a column date by one month whenever a new row is added.
This is what I have, can someone tell me what I'm doing wrong?
CREATE OR REPLACE TRIGGER tg_nextupdate
BEFORE INSERT
ON Vehicle
FOR EACH ROW
BEGIN
IF :NEW.NextUpdate = SYSDATE
THEN
SET NextUpdate = ADD_MONTHS(SYSDATE,1);
END IF;
END;
There is no need of IF-END IF block, whenever a new row is inserted, it will have sysdate. So, just update the NextUpdate to ADD_MONTHS(SYSDATE,1) directly. The check on IF :NEW.NextUpdate = SYSDATE is not required.
CREATE OR REPLACE TRIGGER tg_nextupdate
BEFORE INSERT
ON Vehicle
FOR EACH ROW
BEGIN
:NEW.NextUpdate = ADD_MONTHS(SYSDATE,1);
END;
You can encounter a problem with your code when NextUpdate contains only date, without of hours, minutes and seconds.
Try this:
CREATE OR REPLACE TRIGGER tg_nextupdate
BEFORE INSERT
ON Vehicle
FOR EACH ROW
BEGIN
IF :NEW.NextUpdate = trunc(SYSDATE)
THEN
SET NextUpdate = ADD_MONTHS(SYSDATE,1);
END IF;
END;
Or give us more details about what you want and what you get with your code.
This is what I have, can someone tell me what I'm doing wrong?
Assuming NextUpdate having for default value SYSDATE, as it has already been say, you IF is maybe "not necessary"...
... but, as of myself, I think the real issue is SYSDATE not guaranteeing to return the same value upon each call. If you don't believe me, try that http://sqlfiddle.com/#!4/1f810/2
So, your column might very well be properly initialized by SYSDATE to, say "October, 26 2014 18:50:10+0000". But, in your trigger, SYSDATE might very well return "October, 26 2014 18:50:11+0000". This would be bad luck, I admit. And maybe this is acceptable in your application. But in a more general case, this might easily become a hard to track bug.
Depending your needs, I would suggest one of those three options instead:
1) Assuming SYSDATE is a "magical value" meaning "hey trigger! Compute the right value for NextUpdate":
CREATE OR REPLACE TRIGGER tg_nextupdate
BEFORE INSERT
ON Vehicle
FOR EACH ROW
BEGIN
IF :NEW.NextUpdate <= SYSDATE
THEN
:NEW.NextUpdate := SYSDATE + INTERVAL '1' MONTH;
END IF;
END;
So, any time in the past will trigger the calculation of a new NextUpdate. Including 1s in the past.
2) Override NextUpdate from the trigger -- always:
CREATE TABLE Vehicle (value NUMBER(10),
NextUpdate DATE)
-- ^^^^^^^^^^^^^^^
-- No need for default here
-- as we override values
/
CREATE OR REPLACE TRIGGER tg_nextupdate
BEFORE INSERT
ON Vehicle
FOR EACH ROW
BEGIN
:NEW.NextUpdate := SYSDATE + INTERVAL '1' MONTH;
END;
/
INSERT INTO Vehicle(value) VALUES (1)
/
INSERT INTO Vehicle VALUES (2, TO_DATE('30/10/2014','DD/MM/YYYY'))
/
INSERT INTO Vehicle VALUES (3, TO_DATE('30/12/2014','DD/MM/YYYY'))
/
INSERT INTO Vehicle VALUES (4, NULL)
/
3) Set NextUpdate defaults to SYSDATE + INTERVAL '1' MONTH, allow the user to change that when inserting. If you need it, a trigger might keep the LEAST value (+/- the 1 second error as explained in preamble):
CREATE TABLE Vehicle (value NUMBER(10),
NextUpdate DATE DEFAULT SYSDATE + INTERVAL '1' MONTH)
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- set default to the "most probable" value
/
CREATE OR REPLACE TRIGGER tg_nextupdate
BEFORE INSERT
ON Vehicle
FOR EACH ROW
DECLARE
LimitNextUpdate DATE := SYSDATE + INTERVAL '1' MONTH;
BEGIN
:NEW.NextUpdate := LEAST(LimitNextUpdate,
NVL(:NEW.NextUpdate,LimitNextUpdate));
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- required if the used set the value to NULL
END;
/
INSERT INTO Vehicle(value) VALUES (1)
/
INSERT INTO Vehicle VALUES (2, TO_DATE('30/10/2014','DD/MM/YYYY'))
/
INSERT INTO Vehicle VALUES (3, TO_DATE('30/12/2014','DD/MM/YYYY'))
/
INSERT INTO Vehicle VALUES (4, NULL)
/
You will need to add extra logic (either in the trigger or as a check constraint) in order to reject NextUpdate in the past.

Oracle to_char subquery in Trigger

I have a table (Meeting) with date type attribute (MeetDate) and another varchar2 type attribute (WorkWeek). I'm trying to do an After trigger to fill in the WorkWeek field based on the MeetDate value using the to_char function. Tried the following codes separately and they compile without errors but when I try to insert a row with Null for WorkWeek, it gives me a 'mutating trigger/function may not see it' error. What am I doing wrong here? thanks in advance to any help.
--Code 1
Create or Replace Trigger Update_WorkWeek
After Insert On Meeting
For Each Row
Begin
Update Meeting
Set WorkWeek = (Select to_char(:new.MeetDate, 'YYYY IW') From Dual)
Where MeetID = :new.MeetID;
End;
/
show Errors;
--Code 2
Create or Replace Trigger Update_WorkWeek
After Insert On Meeting
For Each Row
Begin
if :New.WorkWeek is Null then
Update Meeting
Set WorkWeek = (Select to_char(:new.MeetDate, 'YYYY IW') From Dual)
Where MeetID = :new.MeetID;
End if;
End;
/
show Errors;
You just want a trigger to change the value of a column before it gets inserted - and it's on the same row, so you don't need an UPDATE:
Create or Replace Trigger Update_WorkWeek
BEFORE Insert On Meeting
For Each Row
Begin
:new.WorkWeek := to_char(:new.MeetDate, 'YYYY IW');
End;
/
show Errors;
You might want the column kept up-to-date if the MeetDate is changed, i.e.:
Create or Replace Trigger Update_WorkWeek
BEFORE Insert
OR Update OF MeetDate
On Meeting
For Each Row
Begin
:new.WorkWeek := to_char(:new.MeetDate, 'YYYY IW');
End;
/
show Errors;

INSERT TRIGGER IN ORACLE

I am new to triggers in Oracle. I created an EVENT table with this syntax:
CREATE TABLE Event
(event_id NUMBER (3) NOT NULL,
event_date DATE NOT NULL,
venue_id NUMBER (2) NOT NULL,
concert_id NUMBER (3) NOT NULL
);
I want to create a trigger to ensure that concerts cannot run during the month of August. I tried the following code to create the trigger. The trigger was created successfully but after inserting a date with the month of August, it was inserted. This is not suppose to be.
CREATE OR REPLACE TRIGGER check_date
BEFORE INSERT ON event
DECLARE
event_date date;
BEGIN
IF (to_char(event_date, 'mm') = 8) THEN
raise_application_error(-20000, 'Concerts cannot be run during August');
END IF;
END;
First, the trigger needs to be a row-level trigger not a statement-level trigger. You want the trigger to be fired for every row that is inserted not just once for every statement. Declaring the trigger a row-level trigger allows you to see the data for each row that is being inserted.
Second, you don't want to declare a local variable event_date. You want to look at :new.event_date which is the event_date for the row that is being inserted.
If I put those two together
CREATE OR REPLACE TRIGGER check_date
BEFORE INSERT ON event
FOR EACH ROW
BEGIN
IF (to_char(:new.event_date, 'mm') = 8) THEN
raise_application_error(-20000, 'Concerts cannot be run during August');
END IF;
END;
then you'll get the behavior you want
SQL> insert into event values( 1, date '2012-08-01', 1, 1 );
insert into event values( 1, date '2012-08-01', 1, 1 )
*
ERROR at line 1:
ORA-20000: Concerts cannot be run during August
ORA-06512: at "SCOTT.CHECK_DATE", line 3
ORA-04088: error during execution of trigger 'SCOTT.CHECK_DATE'
As a general matter of cleanliness, you also want to compare strings with strings and numbers with numbers. So you would want either
to_number( to_char(:new.event_date, 'mm') ) = 8
or
to_char(:new.event_date, 'fmmm') = '8'
or
to_char(:new.event_date, 'mm') = '08'
change:
IF (to_char(event_date, 'mm') = 8) THEN
to:
IF (to_char(event_date, 'mm') = '08') THEN
You're comparing between string and number.