0

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.

Luc1
  • 27
  • 1
  • 9
  • A suggestion - take your sql statements, and run them individually to see what they results are. They are probably not what you are expecting. Also, what is the purpose of this: "SELECT :NEW.QUANTITA INTO FEED_QUAN FROM FEED;" – OldProgrammer May 20 '17 at 21:37
  • Sorry since I have to translate the table and attributes names from my language I forgot something in the way,now I have fixed it. That statement takes the new QUANTITY value added or updated in the table for the final comparison with the preeciding month average QUANTITY, if it fails the trigger raises the application error. I will follow your advice and look to the single SQL statements in the DB command line. – Luc1 May 20 '17 at 21:46
  • btw it's best to use `varchar2` for all character strings (or `clob` for really long text). `char` is [generally not useful](http://stackoverflow.com/a/42165653/230471). – William Robertson May 21 '17 at 08:17
  • Also your caps lock is on ;) – William Robertson May 21 '17 at 08:19
  • There is a typo: `'MAG'` is not `'MAY'`. Not sure whether that explains the behaviour you describe. – APC May 21 '17 at 09:04
  • @WilliamRobertson since i was using the 3 char long expression for the month i preferred the CHAR. Won't happnen again. – Luc1 May 21 '17 at 12:26
  • I can see it's 3 characters. I was saying `varchar2` is better for that. – William Robertson May 21 '17 at 12:28
  • @APC : sorry since here in italy MAY translates inot MAGGIO oracle return MAG as month from query, in the haste of posting i missed the edit on it. Thanks for noticing. – Luc1 May 21 '17 at 12:29

3 Answers3

1

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.

Community
  • 1
  • 1
  • Thank you sir for your help, I've struggled so much tryning to understand the MERGE sysntax and the trigger mechnics; now I tried to compile the trigger after the table you suggested, after transalting the table where it fires and some :NEW :OLD and date names to match the ones i have in my FEED table. But it returns with this error: `12/5 PL/SQL: SQL Statement ignored 18/30 PL/SQL: ORA-00927: missing equal sign` – Luc1 May 21 '17 at 17:05
  • And besides i wish ask you why there is the `ELSE -1` at the case statement. I suppose it's there when i `DELETE` instead of `INSERTING/UPDATING` the record, but the `CASE` and the entire `MERGE` structure is working under a `IF INSERTING OR UPDATING` condition so i will nevere trigge the `ELSE` of the case. Did i understood wrongly the use of that `CASE` ? – Luc1 May 21 '17 at 17:10
  • The line `SET TOTAL_QUANTITY := TOTAL_QUANTITY +` should be `SET TOTAL_QUANTITY = TOTAL_QUANTITY +`. Regarding the `ELSE -1` - yes, that was left over from some of my initial thinking about the trigger. The entire `CASE` in the `IF INSERTING OR DELETING` section probably isn't needed any more. – Bob Jarvis - Слава Україні May 22 '17 at 04:12
1

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.

Luc1
  • 27
  • 1
  • 9
0
  • 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;
/
MT0
  • 143,790
  • 11
  • 59
  • 117
  • 3
    I suspect that this trigger will fail with an ORA-04091 mutating table error because it's reading from FEED in a row trigger on FEED. – Bob Jarvis - Слава Україні May 21 '17 at 01:57
  • Thank you sir for your detalied answer and explanations. – Luc1 May 21 '17 at 12:48
  • I've compiled the trigger(after translating the tables/attributes in my language) and it went fine without any error. Then i inserted some rows with the the actual month of May for testing it, then changed my System date and added a row with a QUANTITY value exceeding the average(which i knew by using a command line SQL before) and the trigger promptly fired. Then i tried to update case,where i already had a correct row for July month, when the trigger fired as @BobJarvis said early it returned the ORA-4091 error for mutating table . – Luc1 May 21 '17 at 13:00