How do i specify an if statement within a trigger? - sql

i need to insert an "exam entry" row into a table.
But the exam entry cannot be inserted if a student (recognised by the student number sno) is already entered into that exam (recognised by the exam code excode), and the entry also cannot be inserted if the student has more than one exam on the same day (i have an exam table holding the information abotu the exam dates).
I am fairly certain that i should be using an insert trigger function for this and have been looking at:
Example 39-3 from http://www.postgresql.org/docs/9.2/static/plpgsql-trigger.html
so far i have:
INSERT INTO
entry(excode, sno, egrade) VALUES (2, 1, 98.56)
CREATE FUNCTION entry_insert() RETURNS trigger AS $entry_insert$
BEGIN
--Check student is not entered into same exam twice
IF BLA BLA
RAISE EXCEPTION 'A student cannot be be entered into the same exam more than once';
END IF;
--Check student not taking more than one exam on same day
IF BLA BLA
RAISE EXCEPTION 'A student cannot take more than one exam on the same day';
END IF;
END;
$entry_insert$ LANGUAGE PLPGSQL;
CREATE TRIGGER entry_insert BEFORE INSERT ON entry
FOR EACH ROW EXECUTE PROCEDURE entry_insert();
the places where I've put bla bla is where i need the conditions that i cant quite figure out how to meet my conditions.
would love some help?
edit: my exam table
CREATE TABLE exam (
excode CHAR(4) NOT NULL PRIMARY KEY,
extitle VARCHAR(20) NOT NULL,
exlocation VARCHAR(20) NOT NULL, --I'm assuming that an exam will have a location confirmed prior to insertion into the table--
exdate DATE NOT NULL
CONSTRAINT incorrectDate
CHECK (exdate >='01/06/2015' AND exdate <= '30/06/2015'), /* I'm assuming that all exams must have a date confirmed or otherwise the exam wouldn't be inserted into the table*/
extime TIME NOT NULL, -- I'm assuming that an exam will have a time confirmed prior to insertion into the table--
CONSTRAINT incorrect_time
CHECK (extime BETWEEN '09:00:00' AND '18:00:00')
);

You don't need to use triggers for this, you can use normal table constraints, although you will need to define a function.
Your first requirement - that the same student cannot enter the same exam twice - can be checked using a UNIQUE constraint on (excode,sno). In theory this check is redundant because the second check (that a student cannot enter more than one exam per day) would also be violated by that. However, to cater for the possibility of subsequent record updates, this UNIQUE constraint is still needed.
The second requirement can be met using a CHECK constraint. However you have to create a function, because it is not possible to use a subquery inside a CHECK constraint.
Here is an example:
-- Assume we have an exams table. This table specifies the time of each exam
CREATE TABLE exams(excode SERIAL PRIMARY KEY, extime timestamp);
-- Create the entry table. We cannot add the CHECK constraint right away
-- because we have to define the function first, and the table must exist
-- before we can do that.
CREATE TABLE entry(excode int, sno int, egrade FLOAT, UNIQUE(excode,sno));
-- Create a function, which performs a query to return TRUE if there is
-- another exam already existing which this student is enrolled in that
-- is on the same day as the exame identified with p_excode
CREATE FUNCTION exam_on_day(p_excode int, p_sno int) RETURNS bool as $$
SELECT TRUE
FROM entry
LEFT JOIN exams ON entry.excode=exams.excode
WHERE sno=p_sno AND entry.excode != p_excode
AND date_trunc('day',extime)=(
SELECT date_trunc('day', extime) FROM exams WHERE excode=p_excode
);
$$ LANGUAGE SQL;
-- Add check constraint
ALTER TABLE entry ADD CONSTRAINT exam_on_same_day
CHECK(not exam_on_day(excode, sno));
-- Populate some exames.
-- excode 1
INSERT INTO exams(extime) VALUES('2014-12-06 10:00');
-- excode 2
INSERT INTO exams(extime) VALUES('2014-12-06 15:00');
-- excode 3
INSERT INTO exams(extime) VALUES('2014-12-05 15:00');
Now we can try it out:
harmic=> INSERT INTO entry(excode,sno,egrade) VALUES(1,1,98.5);
INSERT 0 1
harmic=> INSERT INTO entry(excode,sno,egrade) VALUES(1,1,50);
ERROR: duplicate key value violates unique constraint "entry_excode_sno_key"
DETAIL: Key (excode, sno)=(1, 1) already exists.
harmic=> INSERT INTO entry(excode,sno,egrade) VALUES(2,1,99);
ERROR: new row for relation "entry" violates check constraint "exam_on_same_day"
DETAIL: Failing row contains (2, 1, 99).
harmic=> INSERT INTO entry(excode,sno,egrade) VALUES(3,1,75);
INSERT 0 1
harmic=> UPDATE entry SET egrade=98 WHERE excode=1 AND sno=1;
UPDATE 1
test=> UPDATE entry SET excode=2 WHERE excode=3 AND sno=1;
ERROR: new row for relation "entry" violates check constraint "exam_on_same_day"
DETAIL: Failing row contains (2, 1, 75).
Note that I named the constraint meaningfully, so that when you get an error you can see why (useful if you have more than one constraint).
Also note that the function used for the SELECT constraint excludes the record being updated from the check (entry.excode != p_excode) otherwise you could not update any records.
You could still do this with triggers, of course, although it would be unnecessarily complicated to set up. The IF condition would be similar to the function created above.

Related

sql oracle - constraint on 2 columns from different tables

I have designed a ticket system booking for flights. I want to add a constraint such that the number of tickets you can insert to be less than number of seats from a flight plane.
Let's say I inserted a flight with a plane with 10 seats. I can insert only 10 tickets for that particular flight. Otherwise, an error message should appear.
I tried to make a trigger using the count function on flight number.
CREATE OR REPLACE TRIGGER trg_ticket_BRIU
BEFORE INSERT OR UPDATE ON Ticket
FOR EACH ROW
DECLARE
l_numberofseats flight.numberofseats%type;
BEGIN
select numberofseats into l_numberofseats
from flight
where flightnumber=:new.flightnumber;
IF :new.count(flightnumber) > l_numberofseats
THEN
raise_application_error(-2000, 'Not enough seats');
END IF;
END;
but I get this error
Trigger TRG_TICKET_BRIU compiled
LINE/COL ERROR
--------- -------------------------------------------------------------
8/5 PLS-00049: bad bind variable 'NEW.COUNT'
Errors: check compiler log
Personally, I would add an AIRCRAFT and a SEAT table:
CREATE TABLE aircraft (
id NUMBER
GENERATED ALWAYS AS IDENTITY
CONSTRAINT aircraft__id__pk PRIMARY KEY,
tail_number VARCHAR2(6)
NOT NULL
CONSTRAINT aircraft__tn__u UNIQUE
CONSTRAINT aircraft__tn_chk CHECK(
REGEXP_LIKE(
tail_number,
'[A-Z]\d{1,5}|[A-Z]\d{1,4}[A-Z]|[A-Z]\d{1,3}[A-Z]{2}'
)
),
manufacturer VARCHAR2(20)
NOT NULL,
model VARCHAR2(20)
NOT NULL,
airline_id CONSTRAINT aircraft__aid__fk REFERENCES airline(airline_id)
NOT NULL
);
CREATE TABLE seat (
id NUMBER
GENERATED ALWAYS AS IDENTITY
CONSTRAINT seat__id__pk PRIMARY KEY,
aircraft_id CONSTRAINT seat__aid__fk REFERENCES aircraft(id)
NOT NULL,
seat_row VARCHAR2(3)
NOT NULL,
seat_column NUMBER
NOT NULL,
CONSTRAINT seat__aid_r_c__u UNIQUE (aircraft_id, seat_row, seat_column)
);
Then your flight table would reference the aircraft:
CREATE TABLE flight (
id NUMBER
GENERATED ALWAYS AS IDENTITY
CONSTRAINT flight__id__pk PRIMARY KEY,
aircraft_id CONSTRAINT flight__aid__fk REFERENCES aircraft(id)
NOT NULL
-- ...
);
And the ticket would reference a flight and a seat:
CREATE TABLE ticket (
id NUMBER
GENERATED ALWAYS AS IDENTITY
CONSTRAINT ticket__id__pk PRIMARY KEY,
flight_id CONSTRAINT ticket__fid__fk REFERENCES flight(id)
NOT NULL,
seat_id CONSTRAINT ticket__sid__fk REFERENCES seat(id)
NOT NULL,
-- ...
CONSTRAINT ticket__fid_sid__u UNIQUE (flight_id, seat_id)
);
Then you can never sell a seat that does not exist on an aircraft and do not need to count the maximum number of tickets and compare it to seats (and the seat has added attributes like its location on the plane that can be displayed on the ticket).
All you need then is to ensure the referential consistency that, for a ticket, the flight and the seat are on the same aircraft; which can be done with a trigger:
CREATE TRIGGER ticket_check_seat_on_flight
BEFORE INSERT OR UPDATE ON ticket
FOR EACH ROW
DECLARE
is_valid NUMBER(1);
BEGIN
SELECT 1
INTO is_valid
FROM flight f
INNER JOIN seat s
ON (f.aircraft_id = s.aircraft_id)
WHERE f.id = :NEW.flight_id
AND s.id = :NEW.seat_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(
-20000,
'Flight and seat are on different aircraft.'
);
END;
/
db<>fiddle here
You can use an AFTER STATEMENT trigger:
CREATE TRIGGER ticket__check_number_of_seats
AFTER INSERT OR UPDATE OR DELETE ON ticket
DECLARE
is_invalid NUMBER(1,0);
BEGIN
SELECT 1
INTO is_invalid
FROM flight f
INNER JOIN (
SELECT flight_id,
COUNT(*) AS tickets_sold
FROM ticket
GROUP BY flight_id
) t
ON f.id = t.flight_id
WHERE t.tickets_sold > f.number_of_seats;
RAISE_APPLICATION_ERROR(
-20000,
'Too many tickets sold for flight.'
);
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
END;
/
It could be made more efficient by using a compound trigger to collate, for each row, the flight_id values into a collection and then, after the statement, only checking the number of tickets for those flights; however, I'll leave that extension as an exercise for the OP.
db<>fiddle here
As others indicated there is no :new.count column. This is because :new (and :old) create a pseudo-row containing exactly the same columns as the table definition. Further you will get a Mutating exception as what you need to count in the flight_number from tickets. However, since that is the table causing he trigger to fire you cannot reference it. So what to do: create a compound trigger, and a supporting Type (nested table). Within it use the after row section to capture the flight_numbers processed. Then in the after statement section you can select count of tickets for each flight. If that count > 0 then raise your exception. ( see Demo )
create type flight_tickets_ntt
is table of integer;
create or replace trigger trg_ticket_ciu
for update or insert on tickets
compound trigger
l_flights flight_tickets_ntt := flight_tickets_ntt();
after each row is
begin
if :new.flight_number not member of l_flights then
l_flights.extend ;
l_flights(l_flights.count) := :new.flight_number;
end if;
end after each row;
after statement is
l_flight_cnt flight.flight_number%type;
begin
select count(*)
into l_flight_cnt
from flight f
where f.number_of_seats <
( select count(*)
from tickets t
where t.flight_number in
( select *
from table (l_flights)
)
);
if l_flight_cnt > 0 then
raise_application_error(-20000, 'Not enough seats');
end if;
end after statement;
end trg_ticket_ciu;
There remains a you need to handle: What happens if an update changes the flight number or perhaps (missing column) the data of the flight.

PSQL Constraint based on column value

Is it possible to have a Constraint but only when one column is set to a particular value. For example take this pseudo-code example of a President which checks to make sure there is never more than 1 President at any time (note, this is NOT valid psql syntax)
CREATE TABLE president (
id BIGSERIAL PRIMARY KEY,
current BOOLEAN NOT NULL,
CONSTRAINT there_can_be_only_one CHECK(COUNT(current=true)<=1)
);
You can use the so called partial index to enforce this specific constraint. In SQL Server they are called filtered indexes.
CREATE UNIQUE INDEX IX ON president (current)
WHERE current = true;
This index should prevent having more than one row in a table with current value set to true, because it is defined as unique.
Unfortunately NO as far as I know and anyway it already tells us,
ERROR: aggregate functions are not allowed in check constraints.
But we can use BEFORE trigger to check that the data you are trying to insert should meets the criteria COUNT(current=true)<=1
CREATE TABLE president (
id BIGSERIAL PRIMARY KEY,
current BOOLEAN NOT NULL
);
---------------------------------------------------------------------
CREATE FUNCTION check_current_flag()
RETURNS trigger
AS $current_president$
DECLARE
current_flag_count integer;
BEGIN
SELECT COUNT(*) FILTER (WHERE current = true )
INTO current_flag_count
FROM president;
IF new.current = true
and current_flag_count >= 1 THEN
RAISE EXCEPTION 'There can be only one current president';
-- RETURN NULL;
ELSE
RETURN NEW;
END IF;
END;
$current_president$ LANGUAGE plpgsql;
---------------------------------------------------------------------
CREATE TRIGGER current_president BEFORE INSERT OR UPDATE ON president
FOR EACH ROW EXECUTE PROCEDURE check_current_flag();
Db<>Fiddle for reference
Note:
You can either throw exception in case of preconditions doesn't match ore simply returning NULL will skip the insert and do nothing. as official document says also here

How to create a conditional trigger

I have a table with an id as auto incremented primary key and another id.
CREATE TABLE tester (
"id" integer PRIMARY KEY AUTOINCREMENT,
"refId" integer DEFAULT 0
);
refId should be able to either be 0 (the default) or reference id if refId > 0 (i.e. act as foreign key).
Now I need two constraints:
A row should only be deletable if its id is not used (referenced?) by any other row's refId
A row should only be deletable if its refId is 0.
From what I have understood, I need to create a trigger that checks for these constraints before a DELETE event happens. And depending on refId's value either abort the delete action or allow it.
However, I have a hard time understanding the syntax for this and how to do a conditional check. But what I have so far (in mind!) is concerning 1.):
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE "refId" = OLD."id") IS NOT NULL;
END;
And concerning 2.)
CREATE TRIGGER no_delete_if_ref
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
IF OLD."refId" > 0 THEN RAISE(ABORT, "cannot delete tester because it refers to an existing tester");
END;
Does this make sense and is valid?
I am totally not sure, to me it does but well, I am all noob.
Also as a last question, can I alternatively combine this into a single trigger? For example would this be a valid query:
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE ("refId" = OLD."id" OR "refId" > 0) ) IS NOT NULL;
END;
You can define a foreign key referring to the same table. Use null instead of 0 for rows without a reference:
create table tester(
id int primary key,
refid int references tester,
check (id <> refid)
);
insert into tester values
(1, null),
(2, null),
(3, 1),
(4, 3);
You need a trigger to ensure that a row which references another one cannot be deleted.
create or replace function before_delete_on_tester()
returns trigger language plpgsql as $$
begin
if old.refid is not null then
raise exception
'Cannot delete: (id)=(%) references (id)=(%)', old.id, old.refid;
end if;
return old;
end $$;
create trigger before_delete_on_tester
before delete on tester
for row execute procedure before_delete_on_tester();
Test:
delete from tester where id = 1;
ERROR: update or delete on table "tester" violates foreign key constraint "tester_refid_fkey" on table "tester"
DETAIL: Key (id)=(1) is still referenced from table "tester".
delete from tester where id = 4;
ERROR: Cannot delete from tester. (id)=(4) references (id)=(3)
CONTEXT: PL/pgSQL function before_delete_on_tester() line 4 at RAISE
In Postgres you have to define a trigger function. Read more:
Overview of Trigger Behavior
Trigger Procedures
Create Trigger

Confused with Oracle Procedure with sequence, linking errors and filling null fields

I am trying to make a procedure that takes makes potential empty "received" fields use the current date. I made a sequence called Order_number_seq that populates the order number (Ono) column. I don't know how to link errors in the orders table to a entry in the Orders_errors table.
this is what i have so far:
CREATE PROCEDURE Add_Order
AS BEGIN
UPDATE Orders
CREATE Sequence Order_number_seq
Start with 1,
Increment by 1;
UPDATE Orders SET received = GETDATE WHERE received = null;
These are the tables I am working with:
Orders table
(
Ono Number Not Null,
Cno Number Not Null,
Eno Number Not Null,
Received Date Null,
Shipped_Date Date Null,
Creation_Date Date Not Null,
Created_By VARCHAR2(10) Not Null,
Last_Update_Date Date Not Null,
Last_Updated_By VARCHAR2(10) Not Null,
CONSTRAINT Ono_PK PRIMARY KEY (Ono),
CONSTRAINT Cno_FK FOREIGN KEY (Cno)
REFERENCES Customers_Proj2 (Cno)
);
and
Order_Errors table
(
Ono Number Not Null,
Transaction_Date Date Not Null,
Message VARCHAR(100) Not Null
);
Any help is appreciated, especially on linking the orders table errors to create a new entry in OrderErrors table.
Thanks in advance.
Contrary to Martin Drautzburg's answer, there is no foreign key for the order number on the Order_Errors table. There is an Ono column which appears to serve that purpose, but it is not a foreign as far as Oracle is concerned. To make it a foreign key, you need to add a constraint much like the Cno_FK on Orders. An example:
CREATE TABLE Order_Errors
(
Ono Number Not Null,
Transaction_Date Date Not Null,
Message VARCHAR(100) Not Null,
CONSTRAINT Order_Errors_Orders_FK FOREIGN KEY (Ono) REFERENCES Orders (Ono)
);
Or, if your Order_Errors table already exists and you don't want to drop it, you can use an ALTER TABLE statement:
ALTER TABLE Order_Errors
ADD CONSTRAINT Order_Errors_Orders_FK FOREIGN KEY (Ono) REFERENCES Orders (Ono)
;
As for the procedure, I'm inclined to say what you're trying to do does not lend itself well to a PROCEDURE. If your intention is that you want the row to use default values when inserted, a trigger is better suited for this purpose. (There is some performance hit to using a trigger, so that's a consideration.)
-- Create sequence to be used
CREATE SEQUENCE Order_Number_Sequence
START WITH 1
INCREMENT BY 1
/
-- Create trigger for insert
CREATE TRIGGER Orders_Insert_Trigger
BEFORE INSERT ON Orders
FOR EACH ROW
DECLARE
BEGIN
IF :NEW.Ono IS NULL
THEN
SELECT Order_Number_Sequence.NEXTVAL INTO :NEW.Ono FROM DUAL;
END IF;
IF :NEW.Received IS NULL
THEN
SELECT CURRENT_DATE INTO :NEW.O_Received FROM DUAL;
END IF;
END;
/
This trigger will then be executed on every single row inserted into the Orders table. It checks if the Ono column was NULL and replaces it with an ID from the sequence if so. (Be careful that you don't ever provide an ID that will later be generated by the sequence; it will get a primary key conflict error.) It then checks if the received date is NULL and sets it to the current date, using the CURRENT_DATE function (which I believe was one of the things you were trying to figure out), if so.
(Side note: Other databases may not require a trigger to do this and instead could use a default value. I believe PostgreSQL, for instance, allows the use of function calls in its DEFAULT clauses, and that is how its SERIAL auto-increment type is implemented.)
If you are merely trying to update existing data, I would think the UPDATE statements by themselves would suffice. Is there a reason this needs to be a PROCEDURE?
One other note. Order_Errors has no primary key. You probably want to have an auto-incrementating surrogate key column, or at least create an index on its Ono column if you only ever intend to select off that column.
There are a number of confusing things in your question:
(1) You are creating a sequence inside a procedure. Does this even compile?
(2) Your procedure does not have any parameters. It just updates the RECEIVED column of all rows.
(3) You are not telling us what you want in the MESSAGE column.
My impression is that you should first go "back to the books" before you ask questions here.
As for your original question
how to link errors in the orders table to a entry in the Orders_errors
table.
This is aleady (correctly) done. The Orders_error table contains an ONO foreign key which points to an order.

sql triggers - want to compare existing row value with value being inserted

I am creating a trigger that should compare of the values being inserted with one that already exists in the table. The old referencer doesn't work here because I am inserting, but how can I reference something that already exists?
Here is my tables and trigger:
create table events(eid char(2) primary key, cid char(2));
create table activities(mid char(2), eid char(2),
primary key (mid, eid),
constraint activities_fk foreign key (eid) references events(eid));
create or replace trigger check_valid
before insert or update on activities
for each row when (old.mid=new.mid)
declare
v_eid char(2);
v_cid char(2);
n_cid char(2);
begin
select eid into v_eid from activities
where mid=:new.mid;
select cid into v_cid from events
where eid=v_eid;
select cid into n_cid from events
where eid=:new.eid;
if v_cid=n_cid then
raise_application_error(-20000, 'Error');
end if;
end check_valid;
/
show errors;
You can't generally select from the table you're inserting into in a trigger. This is the mutating table problem, or as I often call it, the "damn mutating table problem".
Basically, don't do this. It's a bad idea. What happens if you have two sessions operating on the table at once? The trigger fires and neither session sees what the other has done until the commit, which is after the trigger. Then you've got unexpected data in your database.
Tom Kyte says, "when I hit a mutating table error, I've got a serious fatal flaw
in my logic."