Constraining Child Record Based on Parent Record - sql

In a timesheets data model, suppose I have the following parent table:
CREATE TABLE EmployeeInRole (
employeeInRoleId PRIMARY KEY,
employeeId,
roleId,
rate,
effectiveFrom DATE, --from when can this employee assume this role
effectiveTo DATE
);
and the following child table:
CREATE TABLE TimesheetEntry (
startTime DATETIME,
endTime DATETIME,
employeeInRoleId,
CONSTRAINT fk FOREIGN KEY (employeeInRoleId) REFERENCES EmployeeInRole (employeeInRoleId)
);
When I insert into TimesheetEntry, I'd like to make sure that time period falls within the boundaries of the parent record's effectiveFrom/To.
Is it possible to build this constraint into the DDL without use of a trigger, or do I have to maintain this constraint via a trigger or at the application level?

(Here is some info about Oracle only)
It's not possible in Oracle with clear DDL but you can do something like this:
create table t1 (id number primary key, date_from date, date_to date);
create table t2 (id number primary key, date_from date, date_to date, parent_id number references t1(id));
create view v as
select t2.* from t2
where exists (select 1 from t1 where t1.id = t2.parent_id
and t2.date_from between t1.date_from and t1.date_to
and t2.date_to between t1.date_from and t1.date_to)
with check option constraint chk_v;
insert into t1 values (1, sysdate - 5, sysdate); -- OK
insert into v values (1, sysdate - 4, sysdate - 3, 1); -- OK
insert into v values (1, sysdate - 6, sysdate - 3, 1); -- ERROR (WITH CHECK OPTION where-clause violation)
V is updatable view created with CHECK OPTION

"Is it possible to build this constraint into the DDL without use of a trigger,"
It is possible in some RDBMS systems, but it is not possible in SQL.

Related

Can I add a CHECK in my SQL CREATE TABLE script?

I created this table for my database.
CREATE TABLE Reservation
(
Reservation_Name SERIAL UNIQUE PRIMARY KEY,
User VARCHAR(64) DEFAULT 'Member', FOREIGN KEY(User) REFERENCES User(Email)
ON UPDATE CASCADE
ON DELETE SET DEFAULT,
Location INT, FOREIGN KEY(Location) REFERENCES Place(Id_Location)
ON UPDATE CASCADE
ON DELETE NO ACTION,
Start_Date DATE NOT NULL,
Check_In TIME(1) DEFAULT '10:00:00',
End_Date DATE NOT NULL,
Check_Out TIME(1) DEFAULT '18:00:00',
CHECK(Start_Date >= End_Date),
Deleted BOOLEAN DEFAULT FALSE
);
How can I insert a Check that doesn't allow to add a reservation if there's already another one with the same Start_Date and the same End_Date end the same location?
You can use an exclusion constraint:
CREATE EXTENSION btree_gist;
ALTER TABLE reservation ADD EXCLUDE USING gist (
location WITH =,
daterange(start_date, end_date, '[]') WITH &&
);
The extension is required so that you can create a GiST index on an integer column, and && is the "overlaps" operator for range types.
You can symply add a constraint to the table.
Alter Table Reservations Add Constraint unique_reservation Unique(Location,StartDate,EndDate);
You will need a trigger for this. Look at the above code :
CREATE TRIGGER no_overlap
BEFORE INSERT
ON Reservation FOR EACH ROW
BEGIN
SET #overlaps = ( SELECT count(*) FROM Reservation WHERE ( ( NEW.Start_Date >= Start_Date AND NEW.Start_Date <= End_Date AND NEW.Location = Location) || ( NEW.End_Date >= Start_Date AND NEW.End_Date <= End_Date AND NEW.Location = Location)));
IF #overlaps > 0 THEN
SIGNAL SQLSTATE '45000' SET
MYSQL_ERRNO = 31000,
MESSAGE_TEXT = 'Unable to insert an overlapping reservation';
END IF;
END;
INSERT INTO Reservation (Location,Start_Date,End_Date) VALUES(1,'2020-12-13','2020-12-16');
INSERT INTO Reservation (Location,Start_Date,End_Date) VALUES(1,'2020-12-14','2020-12-17');
The first insert will succeed while the second one will fail with the corresponding error message if the dates overlap :
SQL Error [31000] [45000]: (conn=10) Unable to insert an overlapping reservation
By the way, I think you have an error in your table definition. Instead of CHECK(Start_Date >= End_Date), I think you meant CHECK(Start_Date <= End_Date),
Let me know if it helps.
Note : I did this on MariaDB but you can apply the same for any SQL DB.
This works in sql server. I don't have access at the moment to check against postgres. You will need to run in two different batches after creating your table
First
CREATE FUNCTION dbo.HasOverlap (
#locationId int, #start datetime, #end datetime)
RETURNS VARCHAR(5)
AS
BEGIN
IF (SELECT count(*) FROM dbo.Reservation WHERE Location = #locationId
and (
#start between Start_Date and End_Date
or
#end between Start_Date and End_Date
or
(#start <=Start_Date and #end>=End_Date )
)
) >1
return 1
return 0
END
Second
Alter Table dbo.Reservation
with check add Constraint Check_Overlap
check (dbo.HasOverlap(Location, Start_Date, End_Date)=0)
You need to use the composite primary key concept in MySQL Database. Its disable to insert duplicate items in specific columns.
ALTER TABLE table_name ADD CONSTRAINT constraint_name PRIMARY KEY (Start_Date, End_Date, Location)

Changing primary key to new id column

I have an existing database with the primary key being the combination between a name and a timestamp. There is no ID column yet. I would like to add that ID column and change the primary key to ID and timestamp. Rows with the same name should get the same ID.
Since I'm only an intern I would prefer to avoid droping and recreating the table I'm working on since I currently don't have the privileges to do that.
What would be the easiest way to do that?
You can drop a primary key and create a new one according to the docs.
ALTER TABLE table_name
DROP CONSTRAINT primary_key;
Then you'll have to add the ID column and set it as the new Primary Key:
ALTER TABLE table_name
ADD (ID NUMBER);
ALTER TABLE table_name
ADD CONSTRAINT table_pk PRIMARY KEY (ID, Time_Stamp);
Edit: If you would like the ID's to auto increment you will need to create a sequence and a trigger:
CREATE SEQUENCE S
START WITH 1
INCREMENT BY 1;
CREATE OR REPLACE TRIGGER T
BEFORE INSERT ON table_name
FOR EACH ROW
BEGIN
if(:new.ID is null) then
SELECT S.nextval
INTO :new.ID
FROM dual;
end if;
END;
/
ALTER TRIGGER "T" ENABLE;
For completing your entire task you need to
Create ID column
Fill data in ID column according to your requirement
Drop PK
Create PK with ID and Timestamp column
Below is the entire script for the same:
--CREATING THE TABLE
CREATE TABLE TEST (NAME VARCHAR2(200), TIME_STAMP TIMESTAMP,
CONSTRAINT TEST_PK PRIMARY KEY (NAME, TIME_STAMP)
);
-- INSERTING SAMPLE DATA ACCORDING TO YOUR REQUIREMENT -- DUPLICATE NAMES
INSERT INTO TEST
SELECT * FROM
(
SELECT 'NAME1', SYSTIMESTAMP - INTERVAL '1' HOUR FROM DUAL UNION ALL
SELECT 'NAME2', SYSTIMESTAMP - INTERVAL '2' HOUR FROM DUAL UNION ALL
SELECT 'NAME3', SYSTIMESTAMP - INTERVAL '3' HOUR FROM DUAL UNION ALL
SELECT 'NAME4', SYSTIMESTAMP - INTERVAL '4' HOUR FROM DUAL UNION ALL
SELECT 'NAME4', SYSTIMESTAMP - INTERVAL '5' HOUR FROM DUAL UNION ALL
SELECT 'NAME4', SYSTIMESTAMP - INTERVAL '6' HOUR FROM DUAL
);
-- ADDING NEW COLUMN ID
ALTER TABLE TEST ADD ID NUMBER;
-- FILLING THE DATA INTO ID COLUMN
MERGE INTO TEST T
USING (SELECT TIN.NAME, TIN.TIME_STAMP, RANK() OVER (ORDER BY TIN.NAME) RN
FROM TEST TIN) TIN
ON (T.NAME = TIN.NAME AND T.TIME_STAMP = TIN.TIME_STAMP)
WHEN MATCHED THEN
UPDATE SET T.ID = TIN.RN;
-- DROPPING TH EXSITING PK
ALTER TABLE TEST DROP CONSTRAINT TEST_PK;
-- ADDING THE PK WITH ID AND TIME_STAMP
ALTER TABLE TEST ADD CONSTRAINT TEST_PK PRIMARY KEY (ID, TIME_STAMP);
db<>fiddle demo
Cheers!!

MERGE Constraint Error on self referenced table

MERGE is raising an Error
ORA-02291: integrity constraint violated-parent key not found
on inserting into a self referenced table.
If I change query a little bit with no matter, it will work.
Am I do something wrong.
Execution order:
Preparement stuff
First MERGE query (will not work)
Second MERGE query (will work)
The order of the columns in the MERGE query does not matter. You also can run the second MERGE before first.
If you would like to restart your test, start at DROP TABLE TGR.
First some preparement:
DROP TABLE SRC;
CREATE TABLE SRC (
ID VARCHAR(20) PRIMARY KEY,
PARENT VARCHAR(20)
);
INSERT INTO SRC SELECT '1', null FROM DUAL;
INSERT INTO SRC SELECT '2', '1' FROM DUAL;
DROP TABLE TRG;
CREATE TABLE TRG (
ID VARCHAR(20) PRIMARY KEY,
PARENT VARCHAR(20),
CONSTRAINT FK2 FOREIGN KEY(PARENT) REFERENCES TRG(ID)
);
This will not work:
MERGE INTO TRG t USING(
SELECT PARENT, ID FROM SRC
) s ON (t.ID=s.ID)
WHEN NOT MATCHED THEN INSERT(ID,PARENT) VALUES (s.ID,s.PARENT);
But this will work.
MERGE INTO TRG t USING(
SELECT ID, PARENT FROM SRC
) s ON (t.ID=s.ID)
WHEN NOT MATCHED THEN INSERT(ID,PARENT) VALUES (s.ID,s.PARENT);
If you change the datatype of these columns to NUMBER, it will work.
DROP TABLE SRC;
CREATE TABLE SRC (
ID NUMBER PRIMARY KEY,
PARENT NUMBER
);
INSERT INTO SRC SELECT 1, null FROM DUAL;
INSERT INTO SRC SELECT 2, 1 FROM DUAL;
DROP TABLE TRG;
CREATE TABLE TRG (
ID NUMBER PRIMARY KEY,
PARENT NUMBER,
CONSTRAINT FK2 FOREIGN KEY(PARENT) REFERENCES TRG(ID)
);
MERGE INTO TRG t USING(
SELECT PARENT, ID FROM SRC
) s ON (t.ID=s.ID)
WHEN NOT MATCHED THEN INSERT(ID,PARENT) VALUES (s.ID,s.PARENT);
Edit:
Forgot to tell: I am using 12c

Oracle - updating a sorted table

I found an old table without a primary key, and in order to add one, I have to add a new column and fill it with sequence values. I have another column which contains the time of when the record was created, so I want to insert the sequence values to the table sorted by the column with the time.
I'm not sure how to do it. I tried using PL\SQL - I created a cursor for a query that returns the table with an ORDER BY, and then update for each record the cursor returns but it didn't work.
Is there a smart working way to do this?
Thanks in advance.
Another option is just to use a correlated subquery, with the wrinkle of a nested subquery to generate the row number. Setting up some sample data:
create table t42 (datefield date);
insert into t42 (datefield) values (sysdate - 7);
insert into t42 (datefield) values (sysdate + 6);
insert into t42 (datefield) values (sysdate - 5);
insert into t42 (datefield) values (sysdate + 4);
insert into t42 (datefield) values (sysdate - 3);
insert into t42 (datefield) values (sysdate + 2);
select * from t42;
DATEFIELD
---------
12-JUL-12
25-JUL-12
14-JUL-12
23-JUL-12
16-JUL-12
21-JUL-12
Then adding and populating the new column:
alter table t42 add (id number);
update t42 t1 set t1.id = (
select rn from (
select rowid, row_number() over (order by datefield) as rn
from t42
) t2
where t2.rowid = t1.rowid
);
select * from t42 order by id;
DATEFIELD ID
--------- ----------
12-JUL-12 1
14-JUL-12 2
16-JUL-12 3
21-JUL-12 4
23-JUL-12 5
25-JUL-12 6
Since this is a synthetic key, making it match the order of another column seems a bit pointless, but I guess doesn't do any harm.
To complete the task:
alter table t42 modify id not null;
alter table t42 add constraint t42_pk primary key (id);
First of all, create new field and allow null values.
Then, update field from other table or query. Best approach is to use merge statement.
Here a sample from documentation:
MERGE INTO bonuses D
USING (SELECT employee_id, salary, department_id FROM employees
WHERE department_id = 80) S
ON (D.employee_id = S.employee_id)
WHEN MATCHED THEN UPDATE SET D.bonus = D.bonus + S.salary*.01
DELETE WHERE (S.salary > 8000)
WHEN NOT MATCHED THEN INSERT (D.employee_id, D.bonus)
VALUES (S.employee_id, S.salary*.01)
WHERE (S.salary <= 8000);
Finally, set as non null this new field and promote it to primary key.
Here sample sentences:
ALTER TABLE
customer
MODIFY
(
your_new_field varchar2(100) not null
)
;
ALTER TABLE
customer
ADD CONSTRAINT customer_pk PRIMARY KEY (your_new_field)
;
One simple way is to create a new table, with new column an all other columns:
create table newt (
newtID int primary key not null,
. . .
)
Then insert all the old data into it:
insert into newt
select row_number() over (order by <CreatedAt>), t.*
from t
(You can substitute all the columns in, instead of using "*". Having the columns by name is the better practice. This is shorter, plus, I don't know the column names.)
If you alter the table to add the column, then the column will appear at the end. I find that quite awkward for the primary key. If you do that, though, you can update it as:
with t as (select row_number() over (order by <CreatedAt>) as seqnum, t.*
from t
)
update t
set newtID = seqnum

How can I make a copy of a table with a PK?

In an Oracle 10g database, I would like to make a copy of an existing table. I would like it to have the same data and rows as the original table. The original table uses a PK though, so I'm not sure how to copy it and keep them unique.
oracle maintains the pk as a column constraint. you have to copy the table and subsequently create this constraint for the new table.
the following code illustrates how to get your job done.
-- setting up table t1 - this is just for the sake of demonstration
create table t1 (
t_id integer
, t_data varchar2(40)
);
alter table t1 modify ( t_id constraint t1_pk primary key );
insert into t1 values ( 1, 'test');
insert into t1 values ( 2, 'another test');
insert into t1 values ( 3, 'final test');
commit;
-- copying table t1 (definition + contents) and defining the pk
create table t2 as ( select * from t1 );
alter table t2 modify ( t_id constraint t2_pk primary key );
hope this helps,
best regards,
carsten
You can make the copy using
CREATE TABLE dummy_copy as SELECT * FROM dummy//Structure and data
Also you could use dbms_metadata.get_ddl to get the associated constraints of the table
and create it with all the checks
SELECT dbms_metadata.get_ddl( 'TABLE', 'dummy' ) FROM DUAL;
Or you can just do it all in one statement:
create table mike_temp_1 (
col1,
col2,
col3,
col4,
col5,
constraint xpk_mike_temp_1 primary key (col1)
)
as select *
from OLD_POLICY_TERM;
I think the format of specifying column names when using create table as select is a bit fiddly in that I don't believe that you can specify data types (sort of obvious really) but you can specify constraints such as not null, primary key and foreign key.