sql oracle - constraint on 2 columns from different tables - sql

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.

Related

Trigger after insert to check and compare records between table

In an Oracle Database, I need to create some trigger or procedure to treat this case in the most performative way possible (is an extremely large amount of data).
I have a table called ORDER_A that every day receives a full load (its truncated, and all records are inserted again).
I have a table called ORDER_B which is a copy of ORDER_A, containing the same data and some additional control dates.
Each insertion on ORDER_A must trigger a process that looks for a record with the same identifier (primary key: order_id) in table B.
If a record exists with the same order_id, and any of the other columns have changed, an update must be performed on table B
If a record exists with the same order_id, and no values ​​in the other columns have been modified, nothing should be performed, the record must remain the same in table B.
If there is no record with the same order_id, it must be inserted in table B.
My tables are like this
CREATE TABLE ORDER_A
(
ORDER_ID NUMBER NOT NULL,
ORDER_CODE VARCHAR2(50),
ORDER_STATUS VARCHAR2(20),
ORDER_USER_ID NUMBER,
ORDER_DATE TIMESTAMP(6),
PRIMARY KEY (ORDER_ID)
);
CREATE TABLE ORDER_B
(
ORDER_ID NUMBER NOT NULL,
ORDER_CODE VARCHAR2(50),
ORDER_STATUS VARCHAR2(20),
ORDER_USER_ID NUMBER,
ORDER_DATE TIMESTAMP(6)
INSERT_AT TIMESTAMP(6),
UPDATED_AT TIMESTAMP(6),
PRIMARY KEY (ORDER_ID)
);
I have no idea how to do this and what is the best way (with a trigger, procedure, using merge, etc.)
Can someone give me a direction, please?
Here is some pseudo-code to show you a potential trigger based solution that does not fall back into slow row-by-row processing.
create or replace trigger mytrg
for insert or update delete on ordera
compound trigger
pklist sys.odcinumberlist;
before statement is
begin
pklist := sys.odcinumberlist();
end before statement ;
after each row is
begin
pklist.extend;
pklist(pklist.count) := :new.order_id;
end before each row;
after statement is
begin
merge into orderb b
using (
select a.*
from ordera a,
table(pklist) t
where a.order_id = t.column_value
) m
when matched then
update
set b.order_code = m.order_code,
b.order_status = m.order_status,
...
where decode(b.order_code,m.order_code,0,1)=1
or decode(b.order_status,m.order_status,0,1)=1
....
when not matched then
insert (b.order_id,b.order_code,....)
values (m.order_id,m.order_code,....);
end after statement ;
end;
We hold the impacted primary keys, and then build a single merge later, with an WHERE embed to minimise update activities.
If your application allows the update of primary keys, you'd need some additions, but this should get you started

How to implement AFTER INSERT Trigger in Oracle PL/SQL?

I am trying to implement after insert trigger in PLSQL. The goal is to check if there are multiple (>1) rows having specific status for each client. If so I'd like to rise an exception and roll the insertion back.
I am struggling with implementing warning-free query, which causes error during insertion. How could I manage this?
Here is my implemented trigger which I guess needs some changes.
CREATE TRIGGER blatrigger
AFTER INSERT
ON BLATABLE
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
exception_name EXCEPTION;
PRAGMA EXCEPTION_INIT (exception_name, -20999);
BEGIN
if (select count(*) as counter from BLATABLE where CLIENTID = :NEW.CLIENTID and STATUS='PENDING').counter > 1
THEN
raise exception_name;
END IF;
END;
Here is the table itself:
create table BLATABLE
(
ID NUMBER(19) not null primary key,
CLIENTID NUMBER(10),
CREATED TIMESTAMP(6),
STATUS VARCHAR2(255 char)
);
The goal is to check if there are multiple (>1) rows having specific status for each client. If so I'd like to rise an exception and roll the insertion back.
No need for a trigger. It looks like a simple unique constraint should get the job done here:
create table blatable (
id number(19) not null primary key,
clientid number(10),
created timestamp(6),
status varchar2(255 char),
constraint blaconstraint unique (clientid, status)
);
The unique constraint prevents duplicates on (clientid, status) across the whole table. If a DML operation (insert, update) attempts to generate a duplicate, an error is raised and the operation is rolled back.
If, on the other end, you want to allow only one "PENDING" status per user, then you can use a unique index as follows:
create unique index bla_index
on blatable( (case when status = 'PENDING' then clientid end) );
Use a Statement Level Trigger, rather than a Row Level by removing FOR EACH ROW, and converting to your code as below :
CREATE OR REPLACE TRIGGER blatrigger
AFTER INSERT ON BLATABLE
REFERENCING NEW AS NEW OLD AS OLD
DECLARE
counter INT;
exception_name EXCEPTION;
PRAGMA EXCEPTION_INIT(exception_name, -20999);
BEGIN
SELECT MAX(COUNT(*))
INTO counter
FROM BLATABLE
WHERE STATUS = 'PENDING'
GROUP BY CLIENTID;
IF counter > 1 THEN
RAISE exception_name;
END IF;
END;
/
where
the SELECT statement need to be removed from IF .. THEN conditional
Most probably, the mutating table error would raise for Row Level Trigger case
Demo

Oracle - Using a trigger to delete a row

When an insert is made to Condo_assign I am using multiple triggers to add entries to a reserveError table to indicate what the error was. After they are logged in reserveError I am now attempting to use another trigger to delete the record from condo_assign that caused the problem. Basically, the erroneous insert should be logged in ReserveError and Deleted from Condo_assign. The problem is that while my delete trigger compiles and causes no problems, it doesn't appear to do anything. when I select * from condo_assign the erroneous entries are still there.
Condo_Assign table:
CREATE TABLE Condo_Assign (
MID INT
, RID VARCHAR2(3)
, CONSTRAINT Condo_Assign Primary Key (MID,RID)
, CONSTRAINT MID_Assign_FK Foreign Key (MID) references SkiClub (MID)
, CONSTRAINT RID_Assign_FK Foreign Key (RID) references Condo_Reservation (RID)
);
reserveError Table:
CREATE TABLE ReserveError (
Err INT PRIMARY KEY
, MID INT
, RID VARCHAR2(3)
, errorDate DATE
, errorCode VARCHAR2(6)
, errorMsg VARCHAR2(60)
, CONSTRAINT Error_MID_FK FOREIGN KEY (MID) REFERENCES SkiClub
, CONSTRAINT Error_RID_FK FOREIGN KEY (RID) REFERENCES Condo_Reservation
);
Procedure that causes trigger to fire:
CREATE OR REPLACE Procedure addCondo_Assign
(
inMID in Condo_Assign.MID%type
, inRID in Condo_Assign.RID%type
, inPaymentDate in Payment.PaymentDate%type
, inPayment in Payment.Payment%type
)
is
begin
insert into Condo_Assign (MID,RID) values (inMid,inRid);
IF inPayment >= 50 then
insert into Payment (MID,RID,PaymentDate,Payment) values (inMID,inRID,inPaymentDate,inPayment);
ELSE
raise_application_error(-20088,'Deposit less than 50');
end if;
exception
when others then
raise_application_error(-20005,'Cannot add to entry to Condo_Assign Table.');
end addCondo_Assign;
/
Trigger that writes to ReserveError table
-- Trigger to prevent gender mismatchs in room assignment
CREATE OR REPLACE TRIGGER Gender_Assign_Trigger
BEFORE INSERT ON Condo_Assign
FOR EACH ROW
DECLARE
Room_Gender Char(1);
Guest_Gender Char(1);
BEGIN
SELECT Gender
INTO Room_Gender
From Condo_Reservation
WHERE RID = :new.RID;
SELECT Gender
INTO Guest_Gender
FROM SkiClub
WHERE MID = :new.MID;
IF Room_Gender = 'M' AND Guest_Gender = 'F' THEN
addReserveError(:new.MID,:new.RID,SYSDATE,'g00001','Female guest assigned to male room');
ELSIF Room_Gender = 'F' AND Guest_Gender = 'M' THEN
addReserveError(:new.MID,:new.RID,SYSDATE,'g00002','Male guest assigned to female room');
END IF;
END Gender_Assign_Trigger;
/
Trigger that should delete entry from condo_assign:
CREATE OR REPLACE TRIGGER Remove_errors_trigger
after Insert on ReserveError
FOR EACH ROW
BEGIN
DELETE FROM Condo_Assign
WHERE MID = :new.MID and RID = :new.RID;
END remove_errors_trigger;
/
The entire process is started in a before insert trigger.
The code that tries to delete the record won't delete anything because the record has not been inserted yet. There is nothing to delete at the time the delete code is being fired.
Usually checks in triggers prevent the insertion of incorrect data throwing an exception. The code inserting the data will have to handle the exception.
But you already have a procedure that handles the insert. It even performs a check on the deposit. Why not handle the gender check there?

PL/SQL: NO DATA FOUND while updating another table based on conditions

So I have a column on my PAYMENT table that is called Status.. It has a foreign key of another table called reservation with Reservation_ID. The Reservation Table also has a status column and it will only get updated when there is a value in the status column of payment table. So If my status field in payment table has the value "Confirmed", the value for that particular Reservation_ID is supposed to turn to 1.. Otherwise 22. This is how I made the trigger:
CREATE OR REPLACE TRIGGER stats BEFORE INSERT OR DELETE OR UPDATE ON PAYMENT FOR EACH ROW
DECLARE
V_STATUS VARCHAR2(20);
BEGIN
SELECT Status INTO V_STATUS FROM PAYMENT INNER JOIN RESERVATION ON PAYMENT.Reservation_ID=RESERVATION.Reservation_ID WHERE PAYMENT.Reservation_ID=:NEW.Reservation_ID;
IF INSERTING AND V_STATUS='CONFIRMED' THEN
UPDATE RESERVATION SET status=1 WHERE Reservation_ID=:new.Reservation_ID;
ELSIF UPDATING AND V_STATUS='CONFIRMED' THEN
UPDATE RESERVATION SET status=1 WHERE Reservation_ID=:new.Reservation_ID;
ELSE
UPDATE RESERVATION SET status=22 WHERE Reservation_ID=:new.Reservation_ID;
END IF;
END;
So the trigger basically gets compiled but when I try inserting values inside Payment Table, I get the following error:
Error report -
ORA-01403: no data found
ORA-06512: at "ME.STATS", line 4
ORA-04088: error during execution of trigger 'ME.STATS'
create statments for both tables:
CREATE TABLE RESERVATION(RESERVATION_id NUMBER(10) NOT NULL, MEMBER_ID NUMBER(10) CONSTRAINT RE_MEM_fk REFERENCES MEMBER(MEMBER_ID) ON DELETE SET NULL,status NUMBER(10) CONSTRAINT RES_status_fk REFERENCES STATUS(RESERVATION_status_id) ON DELETE SET NULL, CONSTRAINT PK_BOOK PRIMARY KEY(RESERVATION_id));
CREATE TABLE PAYMENT(Payment_ID NUMBER(10) NOT NULL ,RESERVATION_id NUMBER(10) CONSTRAINT Pay_RES_fk REFERENCES RESERVATION(RESERVATION_id) ON DELETE SET NULL, TicketPrice NUMBER(10), ExtraFaciliFees Number(10),TOTAL_AMOUNT Number(10), PromotionalCode VARCHAR2(10), CONSTRAINT PK_PAY PRIMARY KEY(Payment_ID));
First :
CREATE TABLE RESERVATION(
Status NUMBER(10));
SELECT Status INTO V_STATUS
IF INSERTING AND V_STATUS='CONFIRMED'
Could you explain me how you expect a NUMBER to match a string ?
Next (from http://www.dba-oracle.com/sf_ora_01403_no_data_found.htm )
SELECT INTO clauses are standard SQL queries which pull a row or set
of columns from a database, and put the retrieved data into variables
which have been predefined.
If the SELECT INTO statement doesn't return at least on e row,
ORA-01403 is thrown.
So this :
SELECT
Status INTO V_STATUS
FROM PAYMENT p
INNER JOIN RESERVATION r
ON p.Reservation_ID = r.Reservation_ID
WHERE p.Reservation_ID = :NEW.Reservation_ID;
Is likely to output no row at all...
Agree with #Blag, below statement is giving the no data found exception.
In general if you want to know the exact line number the error is pointing to you can refer to the object via DBA_SOURCE or ALL_SOURCES.
SELECT
Status INTO V_STATUS
FROM PAYMENT p
INNER JOIN RESERVATION r
ON p.Reservation_ID = r.Reservation_ID
WHERE p.Reservation_ID = :NEW.Reservation_ID;

Trigger with current date query/where clause

we want to create a trigger which checks if a new measurement (=messung) point lies within the current glacier shape (=umriss).
are tables look like this:
glacier shape (=Umriss)
create table umriss
(
umr_nr number (4) not null,
umr_datum date,
GLST_ID number (4) not null,
shape mdsys.sdo_geometry,
GLETSCHER_ID number (3) not null
)
;
alter table umriss
add constraint umriss_glst_pk
primary key (umr_nr, GLST_ID, GLETSCHER_ID)
;
ALTER TABLE umriss
ADD CONSTRAINT umriss_gletscherstand_fk
FOREIGN KEY (GLST_ID, GLETSCHER_ID)
REFERENCES GLETSCHERSTAND(GLST_ID, GLETSCHER_ID);
new measurement (=Messung)
CREATE TABLE MESSUNG
(
MESS_NR number (4) not null,
MESS_DAT date,
MESS_AKK number (20) NOT NULL,
MESS_SCHMELZ number (20) NOT NULL,
SHAPE mdsys.sdo_geometry,
MESS_BILD blob,
KMPGN_NR NUMBER (4) NOT NULL
);
ALTER TABLE MESSUNG
ADD CONSTRAINT messung_pk
PRIMARY KEY (MESS_NR);
ALTER TABLE MESSUNG
ADD CONSTRAINT messung_messkampagne_fk
FOREIGN KEY (KMPGN_NR)
REFERENCES MESSKAMPAGNE(KMPGN_NR);
Trigger
CREATE OR REPLACE
TRIGGER MESSUNG_in_UMRISS_TRI
BEFORE INSERT OR UPDATE ON MESSUNG
FOR EACH ROW
DECLARE
num_check NUMBER;
BEGIN
SELECT COUNT (*) INTO num_check
FROM UMRISS u
WHERE mdsys.sdo_contains (u.shape, :NEW.point) = 'TRUE';
IF num_check <> 1
THEN
RAISE_APPLICATION_ERROR (=20500, 'Messung in keinem Umriss')
END IF;
END;
How do we iplement the function so the trigger only checks within the most curretn glacier shape?
Thanks for your help!
This will return the one row fron UMRISS which matches the most recent date in the table.
SELECT COUNT (*) INTO num_check
FROM UMRISS u
WHERE mdsys.sdo_contains (u.shape, :NEW.point) = 'TRUE'
AND u.umr_datum = ( select max(d.;umr_datum) from UMRISS d );
This sort of query is the price for keeping historical data in teh same table as the current record.
it seems that POINT ist not defined in your example?
However, you might consider a check constraint because a trigger would only be enforced when a MESSUNG record is inserted or updated, but not when UMRISS is changed
alter table MESSUNG add constraint MESSUNG_CC_CONTAINS check(mdsys.sdo_contains (shape, point) = 'TRUE')
In case you're frequently updating the UMRISS records it might not be feasable at all to enforce such a constraint because Oracle would have to check all MESSUNG records when UMRISS is updated or deleted. Perhaps consider an additional mapping table between MESSUNG and UMRISS that you can update separately.