Check constraint depending on another table (mutually exclusive relation) - sql

I need help with a project, a DB for hotel administration .
I have three tables, bookings, check_ins and cancellations. I consider that any booking can end either as an cancellation or a check-in, but not both simultaneously.
The fact is that I would need a constraint to verify, at check-in, whether the booking was cancelled, and, at cancellation, if people actually came and checked-in.
I used the same id for the three PKs, but it turns out that a check constraint, in Oracle, can't have either a user-defined-function or a select clause inside.
I.e:
I have tried both
ALTER TABLE chek_ins
ADD CONSTRAINT verif_ci CHECK(id_book NOT IN (SELECT id_book FROM bookings));
and creating a function verif_cancelled(id char(a_number))
In the same time I cannot write all of them in the same table, for the guest's identification is unknown until check-in and I use the tables bookings and check-ins as a sort of two intersection tables to link the guest and the room (to solve a M:M relation).
Do you have any suggestions? (this semester I am studying SQL for the first time)
Thank you in advance!

To start with, have a table of possible booking statuses that enumerates all the possible states a booking can be in:
CREATE TABLE statuses (
id NUMBER(1,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT statuses__id__pk PRIMARY KEY,
description VARCHAR2(10)
CONSTRAINT statuses__desc__nn NOT NULL
CONSTRAINT statuses__desc__u UNIQUE
);
INSERT INTO statuses ( description )
SELECT 'Booked' FROM DUAL UNION ALL
SELECT 'Checked-In' FROM DUAL UNION ALL
SELECT 'Annulled' FROM DUAL;
ALTER TABLE statuses READ ONLY;
Then add a status column to your bookings table:
CREATE TABLE Bookings (
id NUMBER(10,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT bookings__id__pk PRIMARY KEY,
status NUMBER(1,0)
CONSTRAINT bookings__status__nn NOT NULL
CONSTRAINT bookings__status__fk REFERENCES statuses ( id ),
CONSTRAINT bookings__id__status__u UNIQUE ( id, status )
)
Then you can add a virtual column to your CheckIns and Annullments tables that have the appropriate statuses so that an entry cannot be made into the table unless the booking is in the correct status:
CREATE TABLE CheckIns (
id NUMBER(10,0)
CONSTRAINT CheckIns__id__pk PRIMARY KEY,
status NUMBER(1,0)
GENERATED ALWAYS AS ( 2 )
CONSTRAINT CheckIns__status__nn NOT NULL
CONSTRAINT CheckIns__status__fk REFERENCES statuses ( id ),
CONSTRAINT CheckIns__id__status__fk
FOREIGN KEY ( id, status )
REFERENCES bookings ( id, status )
);
CREATE TABLE Annullments (
id NUMBER(10,0)
CONSTRAINT annullments__id__pk PRIMARY KEY,
status NUMBER(1,0)
GENERATED ALWAYS AS ( 3 )
CONSTRAINT annullments__status__nn NOT NULL
CONSTRAINT annullments__status__fk REFERENCES statuses ( id ),
CONSTRAINT annullments__id__status__fk
FOREIGN KEY ( id, status )
REFERENCES bookings ( id, status )
);
Then if you try to enter a booking with the Checked-In status into the Annullments table it will fail (and vice versa):
DECLARE
p_id Bookings.id%type;
BEGIN
INSERT INTO bookings ( status )
VALUES( ( SELECT id FROM statuses WHERE description = 'Checked-In' ) )
RETURNING id INTO p_id;
BEGIN
INSERT INTO CheckIns( id )
VALUES ( p_id );
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE( 'Could not check-in' );
END;
BEGIN
INSERT INTO Annullments ( id )
VALUES ( p_id );
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE( 'Could not annul booking' );
END;
END;
/
outputs:
Could not annul booking
and if you try to change a booking to another status while there is a dependent entry in another table then it will fail:
UPDATE Bookings
SET status = ( SELECT id FROM statuses WHERE description = 'Annulled' )
WHERE status = ( SELECT id FROM statuses WHERE description = 'Checked-In' )
Outputs:
ORA-02292: integrity constraint (FIDDLE_CWUTVSMRLOQQQVDQLCGR.CHECKINS__ID__STATUS__FK) violated - child record found
You could further improve the API by creating a package with functions to:
create a booking;
check-in a booking;
annul a booking;
etc.
db<>fiddle

Your check-in table has a booking ID, so that the check-in refers to a booking. This booking ID has probably a unique constraint (maybe it's even the table's primary key), in order to create the desired 1:{0,1} relation. Something along the lines of:
create table check_in
(
booking_id number(9),
check_in_time date,
number_of_persons number(2),
main_person_name varchar2(100),
constraint pk_check_in primary key (booking_id),
constraint fk_check_in_booking foreign key (booking_id) references booking (booking_id)
);
Your annulment table is probably constructed in the same way. However, as these are two different tables that the booking table is not aware of, you can both insert a check-in and an annulment for a booking, which you want to avoid. A check constraint is not possible, because it can only refer to the table itself, not to other tables. (Triggers in the check-in and annulment tables that check the other table to throw an exception in case of an entry there would be possible, but I don't recommend them in this scenario.)
1:1 relatated tables are a rare thing. I see that they are useful here, because you can make their columns mandatory or optional, e.g. the check-in time and main person name may be mandatory (NOT NULL) while the number of persons may be optional. If these were just attributes directly placed in the booking table instead, you could not make check-in time and main person name mandotory, because they must only be mandatory in case of a check-in.
Luckily, with 1:1 you can simply relate them the other way round. I.e. remove the booking ID from the two satellite tables, give them independent primary keys and refer to these primary keys in the booking table instead:
create table check_in
(
check_in_id number(9),
check_in_time date,
number_of_persons number(2),
main_person_name varchar2(100),
constraint pk_check_in primary key (check_in_id)
);
create table annulment
(
annulment_id number(9),
...,
constraint pk_annulment primary key (annulment_id)
);
create table booking
(
booking_id number(9),
room_no number(3),
start_date date,
end_date date,
person_name varchar2(100),
check_in_id number(9),
annulment_id number(9),
constraint pk_booking primary key (booking_id),
constraint fk_booking_check_in foreign key (check_in_id) references check_in (check_in_id),
constraint fk_booking_annulment foreign key (annulment_id) references annulment (annulment_id),
constraint uq_booking_check_in unique (check_in_id),
constraint uq_booking_annulment unique (annulment_id),
constraint chk_booking check (check_in_id is null or annulment_id is null)
);
You see how easy it is to place the desired check constraint in the booking table now.
A last remark: While it is great to have all those integrity checks in place and be able to, say, select all annulments placed in March, you might be able to do with a much simpler database. Instead of two satellite tables, you could just put the information in single columns, e.g. a CLOB, JSON, or Oracle object. It would be like using a mere note rather than a form in real live. Less reliable, but maybe sufficient for the job. The database would reduce to a single table:
create table booking
(
booking_id number(9),
room_no number(3),
start_date date,
end_date date,
person_name varchar2(100),
check_in_data varchar2(4000),
annulment_data varchar2(4000),
constraint pk_booking primary key (booking_id),
constraint chk_booking check (check_in_data is null or annulment_data is null)
);
As mentioned, the DBMS can not guarantee here the check-in and annulment data to be complete (i.e. to the DBMS it makes no difference if annulment_data contains 'Cancelled on Feb 9, 2019 by Mr. Miller, because of sickness' or just 'Cancelled by Mr. Miller' or even 'dum deedle dum'), but maybe you are fine with this - your app will make sure only complete data gets written to the database.

Related

How can set unique key checking by another table

i have create three tables: supplier, item and purchase. supplier id has relation with item table and item id has relation with purchase table. I do not want to insert itemid on purchase table in same supplier item. How can I set constraint?
CREATE TABLE csupplier(
supid NUMBER(10) PRIMARY KEY ,
supname VARCHAR2(30)
);
CREATE TABLE ctitem(
itemid NUMBER(10) PRIMARY KEY,
itemname VARCHAR2(50),
supid NUMBER(10)
);
ALTER TABLE CTITEM
ADD CONSTRAINT CTITEM_FK1 FOREIGN KEY(SUPID )REFERENCES CSUPPLIER(SUPID );
CREATE TABLE cPurchase(
purchaseid NUMBER(10) PRIMARY KEY,
itemid NUMBER(10),
purchaseqty NUMBER(10)
);
ALTER TABLE CPURCHASE
ADD CONSTRAINT CPURCHASE_FK1 FOREIGN KEY(ITEMID )REFERENCES CTITEM(ITEMID )
i don not want insert item-1 and item-3 in a same time under purchase
The problem is Oracle does not understand concept of at the same time. It understands transactions, it understands DML statements, it understands unique keys. So we need to frame your question in terms Oracle can understand: for instance, a given purchase cannot have more than one item from the same supplier.
Your first problem is that your data model can't support such a rule. Your cpurchase table has a primary key of purchaseid which means you have one record per item purchased. There is no set of purchased items against which we can enforce a rule. So, the first thing is to change the data model:
CREATE TABLE cPurchase(
purchaseid NUMBER(10) PRIMARY KEY );
CREATE TABLE cPurchaseItem(
purchaseid NUMBER(10),
itemid NUMBER(10),
purchaseqty NUMBER(10)
);
ALTER TABLE CPURCHASEITEM
ADD CONSTRAINT CPURCHASEITEM_PK PRIMARY KEY(PURCHASEID,ITEMID);
ALTER TABLE CPURCHASEITEM
ADD CONSTRAINT CPURCHASEITEM_FK1 FOREIGN KEY(PURCHASEID )REFERENCES CPURCHASE;
ALTER TABLE CPURCHASE
ADD CONSTRAINT CPURCHASE_FK2 FOREIGN KEY(ITEMID )REFERENCES CTITEM(ITEMID );
Now we have a header-detail structure which assigns multiple items to one purchase, which means we can attempt to enforce the rule.
The next problem is that supplierid is not an attribute of cpurchaseitem. There is no way to build a check constraint on a table or column which executes a query on another table. What you are after is a SQL Assertion, which is a notional construct that would allow us to define such rules. Alas Oracle (nor any other RDBMS) supports Assertions at the moment.
So that leaves us with three options:
Go procedural, and write a transaction API which enforces this rule.
Denormalise cpurchaeitem to include supplierid then build a unique constraint on (purchaseid, supplierid). You would need to populate supplierid whenever you populate cpurchaseitem.
Write an after statement trigger:
(Warning: this is coded wildstyle and may contain bugs and/or compilation errors.)
create or replace trigger cpurchaseitem_trg
after insert or update on cpurchaseitem
declare
rec_count number;
begin
select count(*)
into rec_count
from cpurchaseitem pi
join citem I on pi.itemid = i.itemid
group by pi.purchaseid, i.supplierid having count(*) > 1;
if rec_count > 0 then
raise_application_error(-20000
, 'more than one item for a supplier!');
end if;
end;
Frankly none of these solutions is especially appealing. The API is a solid solution but open to circumvention. The trigger will suffer from scaling issues as the number of purchases grows over time (although this can be mitigated by writing a compound trigger instead, left as an exercise for the reader). Denormalisation is the safest (and probably most performative) solution, even though it's not modelling best practice.
There are 2 solutions to your problem:
1. Alter the table cPurchase and add the supid column in the table. and make the unique key on this column. This will solve your problem.
CREATE TABLE cPurchase(
purchaseid NUMBER(10) PRIMARY KEY,
itemid NUMBER(10),
purchaseqty NUMBER(10),
supid NUMBER(10) UNIQUE KEY
);
If alter is not possible on this table, write a row level, Before Insert/update trigger. In this trigger write the logic to find the Supid based on the Item_id ctitem and them find any item on this supplier exists in your purchase table.
CREATE [ OR REPLACE ] TRIGGER SUP_CHECK
BEFORE INSERT
ON cPurchase
FOR EACH ROW
DECLARE
L_COUNT NUMBER;
BEGIN
SELECT COUNT(*) INTO L_COUNT
FROM cPurchase c
WHERE C.itemid in (Select itemid from ctitem ct where ct.supid = (Select supid
from ctitem where itemid = :new.itemid) );
EXCEPTION
WHEN ...
-- exception handling
END;

Oracle SQLDeveloper , using computed column with another table

I'm trying to create a database which will be used by an app made for an auto repair shop.
I have a 'Services' table where several services are listed with the corresponding price (e.g oil change , 30$). I have another table "Repairs" where I'm storing information about the client's repairs.This table has a column price which must be calculated using each of the services prices in the table 'Services'.
I am familiar with the concept of using 'computed columns' , but it appears that I can't compute a column using values from another table.
Am i missing something here or how should i approach the problem?
Having a column named "Price" in the table "Repairs", if you are trying to store the total price of all services provided to a customer, you shouldn't do that.
Why don't you try to insert a service record to "Repairs" table for each service provided to a customer with a customer id and repair id. Each service will point to the "Services" table with a foreign key. This way you can query the total price of a repair or you can create a view that will show the total prices of all repairs.
Here are create scripts for sample tables:
-- Table named SERVICES:
CREATE TABLE "SERVICES"
(
"SERVICE_ID" NUMBER NOT NULL ENABLE,
"SERVICE_NAME" VARCHAR2(20 BYTE),
"SERVICE_PRICE" NUMBER,
CONSTRAINT "SERVICES_PK" PRIMARY KEY ("SERVICE_ID")
)
-- Table named CUSTOMERS:
CREATE TABLE "CUSTOMERS"
(
"CUSTOMER_ID" NUMBER NOT NULL ENABLE,
"CUSTOMER_NAME" VARCHAR2(20 BYTE),
CONSTRAINT "CUSTOMERS_PK" PRIMARY KEY ("CUSTOMER_ID")
)
-- Table named REPAIRS:
CREATE TABLE "REPAIRS"
(
"REPAIR_ID" NUMBER NOT NULL ENABLE,
"CUSTOMER_ID" NUMBER NOT NULL ENABLE,
"REPAIR_DATE" DATE,
CONSTRAINT "REPAIRS_PK" PRIMARY KEY ("REPAIR_ID")
)
-- Table named REPAIR_SERVICES:
CREATE TABLE "REPAIR_SERVICES"
(
"REPAIR_ID" NUMBER NOT NULL ENABLE,
"SERVICE_ID" NUMBER NOT NULL ENABLE,
CONSTRAINT "REPAIR_SERVICES_FK_1" FOREIGN KEY ("REPAIR_ID") REFERENCES "HR"."REPAIRS" ("REPAIR_ID") ENABLE,
CONSTRAINT "REPAIR_SERVICES_FK_2" FOREIGN KEY ("SERVICE_ID") REFERENCES "HR"."SERVICES" ("SERVICE_ID") ENABLE
)
If you structure your tables like the ones above, you can use the script below to create a view which will get you the total cost of repairs including all services. Or you can just use the script to make a query.
CREATE VIEW V_REPAIR_PRICES AS (
SELECT REPAIRS.REPAIR_ID, REPAIRS.REPAIR_DATE, CUSTOMERS.CUSTOMER_NAME, T1.TOTAL_PRICE
FROM REPAIRS
INNER JOIN CUSTOMERS ON REPAIRS.CUSTOMER_ID = CUSTOMERS.CUSTOMER_ID
INNER JOIN
(
SELECT REPAIR_SERVICES.REPAIR_ID, SUM(SERVICES.SERVICE_PRICE) as TOTAL_PRICE
FROM REPAIR_SERVICES
INNER JOIN SERVICES ON REPAIR_SERVICES.SERVICE_ID = SERVICES.SERVICE_ID
GROUP BY REPAIR_SERVICES.REPAIR_ID
) T1
ON REPAIRS.REPAIR_ID = T1.REPAIR_ID
)

Database design issue. Column value pointing to different tables

Description of what I have to do
I have a table that should be related to Table1 OR Table2 OR Table3
For instance, there's a table Employees and it has:
Id,
Name,
Address,
Age,
Salary,
EmployerId
The second table is RegisterEmployeeRequirements:
Id,
RequirementType,
EmployerId,
EntryId.
Where requirement type could be CreditStatusRequirement or EmployeeDegreeRequirement).
The Problem: CreditStatusRequirement includes both the CreditStatus and the date it was acquired (to check if it was in the last year). I also have additional table which is named CreditStatusRequirements with columns:
CreditStatus,
DateTimeAcquired
On the other hand, the degree requirement which has the following properties: DegreeName and MinGpa.
To solve this I created another table with these properties. If the requirement type in the RegisterEmployeeRequirements is CreditStatusRequirement I will use the entryId column to look at the CreditStatusRequirements table and then to check if it is completed.
Otherwise, if it is EmployeeDegreeRequirement, I will use the entryId column to look into DegreeRequirements table. I suppose it is not a good practice to use such a column like entryId.
What is the way to solve this architecture issue?
It's fairly simple. Don't have the FK equivalent in the RegisterEmployeeRequirements table. Have the FKs in each of the Credit and Degree requirements details tables to RegisterEmployeeRequirements.
Like this:
create table RegisterEmployeeRequirements(
EmployeeId int references ( ID ),
RequirementType char( 1 ) not null,
..., -- Other common fields
constraint PK_RegisterEmployeeRequirements primary key( EmployeeID, RequirementType ),
constraint FK_RegisterEmployeeRequirements_Empe( EmployeeId )
references Employees( ID ),
constraint FK_RegisterEmployeeRequirements_Type( RequirementType )
references RequirementTypes( ID ),
);
Notice the key is the combination of employee id and requirement id. This ensures each employee can have no more than one of each of the two defined requirements. I assume that is in line with your database requirements.
Then each requirements detail table can be defined something like this:
create table CreditRequirements(
EmployeeId int primary key,
RequirementType char( 1 ) check( CreditType = 'C' ),
Status ...,
Acquired datetime,
constraint FK_CreditRequirements_Emp foreign key( EmployeeID, RequirementType )
references RegisterEmployeeRequirements( EmployeeID, RequirementType )
);
create table DegreeRequirements(
EmployeeId int primary key,
RequirementType char( 1 ) check( DegreeType = 'D' ),
DegreeName varchar( 64 ),
MinGPA float,
constraint FK_DegreeRequirements_Emp foreign key( EmployeeID, RequirementType )
references RegisterEmployeeRequirements( EmployeeID, RequirementType )
);
An entry in the credit details table can only be made for an employee that has a credit type entry made in the RegisterEmployeeRequirements table. Same for the degree details table for a degree type entry in RegisterEmployeeRequirements. No more than one of each type of requirement can be inserted in RegisterEmployeeRequirements and only one entry for each employee can be inserted in each of the details table.
Your data integrity is sound and the design is scalable. If a third requirement type is created, the type entry is inserted in the RequirementTypes table and a new details table is created for that type. None of the existing tables would need altering.
Why not just use separate tables for credit status and degree requirements, and remove the need for a RequirementType column?
table RegisterEmployeeCreditStatusRequirements has:
Id, EmployerId, CreditStatus, DateTimeAcquired
table RegisterEmployeeEmployeeDegreeRequirements has:
Id,EmployerId,DegreeName,MinGpa

Architecture for audits

I am designing a database to capture audits that my company performs. I am having a bit of trouble finding an efficient way to capture all of the audit points without making 60 columns in a single table. Is there an efficient way to capture multiple data points in a single column and still be able to query without trouble.
Each audit may have anywhere from 0 to 60 unique citations. I will make a reference table to hold every regulatory citation, but how do I design the central table so that the 'citation' column can have , or , or any number of other combinations?
I usually try to keep auditing info in a single table.
In order to do this, I go something like this:
TABLE: Audit
**Id** (PK)
**EntityClass** (the Class, or type, or whatever you want to identify your entities by)
**EntityId** (the id of the entity in it's own table)
**PropertyChanged** (the name of the property of the entity that changed)
**OldValue** (the old value of the property)
**NewValue** (the revised value of the property)
**TimeStamp** (moment of the revision)
**RevisionType** (transaction type: Insert, Update, Delete)
This is the simplest schema, you can build on that with additional columns if you wish.
Hope this helps. Cheers!
In this example, I'm assuming, since you refer to a specific number if citations, there is -- or can be -- a taxonomic table holding 60 definitions or references, one for each kind of citation.
The Audits table contains the relevant info about each audit. I'm guessing most of this, but note there is no reference to any citation.
create table Audits(
ID int identity( 1, 1 ),
Started date,
Completed date,
CustomerID int,
AuditorID int, -- More than one possible auditor? Normalize.
VerifierID int,
Details ...,
constraint PK_Audits primary key( ID ),
constraint FK_Audits_Customer( foreign key( CustomerID )
references Customers( ID ),
constraint FK_Audits_Auditor( foreign key( AuditorID )
references Auditors( ID ),
constraint FK_Audits_Verifier( foreign key( VerifierID )
references Auditors( ID ),
constraint CK_Audits_Auditor_Verifier check( AuditorID <> VerifierID )
);
The AuditCitations table contains each citation for each audit, one entry for each citation. Note that the PK will prevent the same audit from having more than one reference to the same citation (if, of course, that is your rule).
create table AuditCitations(
AuditID int,
CitID int,
Details ...,
constraint FK_AuditCitations_Audit( foreign key( AuditID )
references Audits( ID ),
constraint FK_AuditCitations_Citation( foreign key( CitID )
references Citations( ID ),
constraint PK_AuditCitations primary key( AuditID, CitID )
);
A citation may well have its own auditor and verifier/checker or just about anything that applies to the particular citation. This example mainly just shows the relationship between the two tables.

Schema for db to store current and edit history of multi-field data

I'm trying to design a database schema to keep records on for sale / want ads. I'd like to be able to query, for instance "what was the asking price of posts with this keyword in the title for the past 6 months?"
But the posts themselves can be updated and modified by the posters, so I'd also like to be able to ask questions like, "What revisions did this specific post go through? How did the asking price change?"
My plan with the schema below was to have separate tables for each field I took from the post, keyed by post id and the time they were last updated. I'd also have one table (the last one) that showed how the post looked at each update.
So if there was a post with id, date, price, title, and description, there would be one entry in the post_id, post_date, post_price, post_title, and description tables, as well as one in the post_state table. If the user updated just the price, new entries would be added to the post_date, post_price and post_states table, and the new row in the post_states table would show the new price, but the original values in the other fields.
Schema follows. The problem I'm getting is that the post_states table cannot be created, because the fields in the tables it references are not unique. I'm new at this, and it may be that I'm doing it completely wrong. Also, I'm using PostgreSQL.
CREATE TABLE IF NOT EXISTS post_id (
id integer PRIMARY KEY,
url varchar
);
CREATE TABLE IF NOT EXISTS modify_date (
mod_date time,
pid integer REFERENCES post_id,
UNIQUE (mod_date, pid),
PRIMARY KEY(mod_date, pid)
);
CREATE TABLE IF NOT EXISTS post_price (
pid integer,
mod_date time,
price money,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_title (
pid integer,
mod_date time,
title varchar,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_location (
pid integer,
mod_date time,
location varchar,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_email (
pid integer,
mod_date time,
email varchar,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_description (
pid integer,
mod_date time,
description varchar,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_state (
pid integer,
mod_date time,
title varchar REFERENCES post_title(title),
description varchar REFERENCES post_description(description),
price money REFERENCES post_price(price),
location varchar REFERENCES post_location(price),
email varchar REFERENCES post_email(email),
url varchar REFERENCES post_id(url),
FOREIGN KEY(mod_date, pid) references modify_date
);
Two quick points.
For revision tracking as long as you have a date tracked and a revision number, I think you would do well to look at table_log, which can be used to log previous versions of the log.
Secondly not having primary keys on most of your tables is asking for trouble down the road. I prefer natural primary keys where applicable but you should at least add a surrogate key if this is not possible.