Can a Check constraint relate to another table? - sql

Let's say I have one table called ProjectTimeSpan (which I haven't, just as an example!) containing the columns StartDate and EndDate.
And that I have another table called SubProjectTimeSpan, also containing columns called StartDate and EndDate, where I would like to set a Check constraint that makes it impossible to set StartDate and EndDate to values "outside" the ProjectTimeSpan.StartDate to ProjectTimeSpan.EndDate
Kind of a Check constraint that knows about another tables values...
Is this possible?

In response to your comment on GSerg's answer, here's an example check constraint using a function:
alter table YourTable
add constraint chk_CheckFunction
check (dbo.CheckFunction() = 1)
Where you can define the function like:
create function dbo.CheckFunction()
returns int
as begin
return (select 1)
end
The function is allowed to reference other tables.

You can create a user-defined function that does the check and returns 1 or 0, then create a check constraint on it, providing project id and the dates as the parameters.

Make a compound key of the ProjectTimeSpan table's key combined with the StartDate and EndDate columns, then use this compound key for your foreign key reference in your SubProjectTimeSpan table. This will give you the ability to write the necessary row-level CHECK constraints in the SubProjectTimeSpan table e.g.
CREATE TABLE ProjectTimeSpan
(
project_ID INTEGER NOT NULL UNIQUE, -- key
StartDate DATE NOT NULL,
EndDate DATE NOT NULL,
CHECK (StartDate < EndDate),
UNIQUE (project_ID, StartDate, EndDate) -- compound key
-- other project columns here...
);
CREATE TABLE SubProjectTimeSpan
(
project_ID INTEGER NOT NULL,
StartDate DATE NOT NULL,
EndDate DATE NOT NULL,
FOREIGN KEY (project_ID, StartDate, EndDate)
REFERENCES ProjectTimeSpan (project_ID, StartDate, EndDate)
ON DELETE CASCADE
ON UPDATE CASCADE,
sub_StartDate DATE NOT NULL,
sub_EndDate DATE NOT NULL,
CHECK (sub_StartDate < sub_EndDate),
CHECK (StartDate <= sub_StartDate), -- sub project can't start before main project
CHECK (sub_EndDate <= EndDate) -- sub project can't end after main project
-- other sub project columns here...
);

You certainly can do this as many answers have shown. However, you should be aware that SQL Server seems to have trouble with CHECK CONSTRAINTs that use UDFs:
https://dba.stackexchange.com/questions/12779/how-are-my-sql-server-constraints-being-bypassed

You need to add constraint on the parent and the children table because the subproject can't be out of the project range but the project range can't move out of all the subproject too.
In these kind of situations, you should defer the check of the constraint on an upper level (webservice, application) with a transaction to ensure your data are in a valid state after multiple query on both table !

It is absolutely possible, and actually quite simple. As per your example:
create or alter function dbo.Check_SubProjectTimeSpan_ProjectTimeSpan_Dates(
#ProjectTimeSpanId int
, #StartDate date
, #EndDate date
)
returns bit
as
begin
if exists (select *
from dbo.ProjectTimeSpan as pts
where pts.Id = #ProjectTimeSpanId
and pts.StartDate >= #StartDate
and pts.EndDate <= #EndDate)
begin
return 1
end
return 0
end
go
alter table dbo.SubProjectTimeSpan add constraint
CK_SubProjectTimeSpan_ProjectTimeSpan_ProjectTimeSpanId_StartDate_EndDate
check (
dbo.Check_SubProjectTimeSpan_ProjectTimeSpan_Dates(
ProjectTimeSpanId, StartDate, EndDate) = 1
)
Please be aware however, that this will not be particularly performant.

Related

Can't use UDF as constraint in MariaDB

I'm running MariaDB 10.3.17 and I'm trying to add a constraint to an existing table. The constraint uses a UDF - which should be allowed.
Here's my table and UDF.
CREATE OR REPLACE TABLE real_estate.sample_two_expected_output (
u_id int (9) NOT NULL,
first_date date NOT NULL,
last_date date NOT NULL,
days int AS (DATEDIFF(last_date,first_date)+1),
address varchar(50),
price varchar(50),
--Constraints
CONSTRAINT dates CHECK (last_date >= first_date),
PRIMARY KEY (u_id,first_date));
DELIMITER //
USE real_estate;
CREATE OR REPLACE FUNCTION overlap(
u_id INT,
first_date DATE,
last_date DATE
) RETURNS INT DETERMINISTIC
BEGIN
DECLARE valid INT;
SET valid = 1;
IF EXISTS(SELECT * FROM real_estate.sample_two_expected_output t WHERE t.u_id = u_id AND first_date <= t.last_date AND t.first_date <= last_date) THEN SET valid = 0;
ELSE SET valid = 1;
END IF;
RETURN valid;
END; \\
DELIMITER;
I try to add this function as a constraint in the table.
ALTER TABLE real_estate.sample_two_expected_output ADD CONSTRAINT overlap CHECK(overlap(u_id,first_date,last_date)=1);
However I get the below error message and I don't know why.
EXECUTE FAIL:
ALTER TABLE real_estate.sample_two_expected_output ADD CONSTRAINT overlap CHECK(overlap(u_id,first_date,last_date)=1);
Message :
Function or expression '`overlap`()' cannot be used in the CHECK clause of `overlap`
In general you can use any deterministic user defined function (UDF) but not a stored function (SF) in constraints like DEFAULT, CHECK, etc.
A big difference between UDFs and SFs is the fact that a UDF is usually written in C/C++ while a SF is written in SQL. That means it is not possible to execute SQL code in a UDF within the same connection, which would lead to significant problems, as your SF shows:
Depending on the storage engine ALTER TABLE locks the entire table, parts of it or creates a temporary copy. I cannot imagine a way to execute the SQL statement SELECT * FROM real_estate.sample_two_expected_output t WHERE t.u_id = u_id .. in your SF while the table is locked or reorganized.

SQL - How do you use a user defined function to constrain a value between 2 tables

First here's the relevant code:
create table customer(
customer_mail_address varchar(255) not null,
subscription_start date not null,
subscription_end date, check (subscription_end !< subcription start)
constraint pk_customer primary key (customer_mail_address)
)
create table watchhistory(
customer_mail_address varchar(255) not null,
watch_date date not null,
constraint pk_watchhistory primary key (movie_id, customer_mail_address, watch_date)
)
alter table watchhistory
add constraint fk_watchhistory_ref_customer foreign key (customer_mail_address)
references customer (customer_mail_address)
on update cascade
on delete no action
go
So i want to use a UDF to constrain the watch_date in watchhistory between the subscription_start and subscription_end in customer. I can't seem to figure it out.
Check constraints can't validate data against other tables, the docs say (emphasis mine):
[ CONSTRAINT constraint_name ]
{
...
CHECK [ NOT FOR REPLICATION ] ( logical_expression )
}
logical_expression
Is a logical expression used in a CHECK constraint and returns TRUE or
FALSE. logical_expression used with CHECK constraints cannot
reference another table but can reference other columns in the same
table for the same row. The expression cannot reference an alias data
type.
That being said, you can create a scalar function that validates your date, and use the scalar function on the check condition instead:
CREATE FUNCTION dbo.ufnValidateWatchDate (
#WatchDate DATE,
#CustomerMailAddress VARCHAR(255))
RETURNS BIT
AS
BEGIN
IF EXISTS (
SELECT
'supplied watch date is between subscription start and end'
FROM
customer AS C
WHERE
C.customer_mail_address = #CustomerMailAddress AND
#WatchDate BETWEEN C.subscription_start AND C.subscription_end)
BEGIN
RETURN 1
END
RETURN 0
END
Now add your check constraint so it validates that the result of the function is 1:
ALTER TABLE watchhistory
ADD CONSTRAINT CHK_watchhistory_ValidWatchDate
CHECK (dbo.ufnValidateWatchDate(watch_date, customer_mail_address) = 1)
This is not a direct link to the other table, but a workaround you can do to validate the date. Keep in mind that if you update the customer dates after the watchdate insert, dates will be inconsistent. The only way to ensure full consistency in this case would be with a few triggers.

How do I validate these columns, do i need more complex triggers?

I'm trying to validate the columns in the table ProjectDetails.TimeCards
create table ProjectDetails.TimeCards(
Time_Card_ID int identity (55,15),
Employee_ID int foreign key references HumanResources.Employees(Employee_ID),
Date_Issued date, --DateIssued should be greater than the current date and the project start date (Project start date is from another table).
Days_Worked int constraint chk_Days_Worked check(Days_Worked > '0'),
Project_ID int foreign key references ProjectDetails.Projects(Project_ID),
Billable_hours int constraint chk_Billable_Hours check (Billable_Hours > '0'),
Total_Cost money, -- should be automatically calculated by using the following formula: TotalCost=Billable Hours * BillingRate (billing rate is from another table)
Work_Code_ID int foreign key references ProjectDetails.WorkCodes(Work_Code_ID)
);
I tried building a trigger, but was only able to get the trigger to fire if the Date_Issued was less than the current date
CREATE TRIGGER dateissued
ON ProjectDetails.TimeCards
FOR INSERT
AS
DECLARE #ModifiedDate date
SELECT #ModifiedDate = Date_Issued FROM Inserted
IF (#ModifiedDate < getdate())
BEGIN
PRINT 'The modified date should be the current date. Hence, cannot insert.'
ROLLBACK TRANSACTION -- transaction is to be rolled back
END
i need the trigger to fire if the Date issued is less than the current date and also if date issued is less than the start_date. As for the billing rate calculation i'm lost there.
This is the other table
create table ProjectDetails.Projects(
Project_ID int identity (0100, 01) primary key, -- Primary key
Project_Name char (50) not null,
Start_Date date not null, -- the start date i'm referring to
End_Date date not null,
constraint CheckEndLaterThanStart check (End_Date > Start_Date),
Billing_Estimate money constraint chk_Billing_Estimate check (Billing_Estimate > '1000'),
Client_ID int Foreign Key references CustomerDetails.Clients(Client_ID)
);
I believe this provides the logic you are after. There are a couple of comments in the code for you:
CREATE TRIGGER trg_chk_Date_Issued ON ProjectDetails.TimeCards
FOR INSERT, UPDATE --I assume on need on UPDATE as well, as otherwise this won't validate
AS BEGIN
IF EXISTS(SELECT 1
FROM inserted i
JOIN ProjectDetails.Projects P ON i.Project_ID = P.Project_ID
WHERE i.Date_Issued < CONVERT(date,GETDATE())
OR i.Date_Issued < P.Start_Date) BEGIN
RAISERROR(N'Date Issued cannot be less than the current date or the Project Start Date.', 10,1); --Raised an error, rather than using PRINT
ROLLBACK;
END
END
Note that you may want a different trigger for UPDATE, as if you are updating the row, and Date_Issued has a value lower than the date of the UPDATE, the statement will fail.

DB2SQL Trying to create a trigger with function (Calculate days)

So for now, I created 2 tables, Booking and BookingDetails, I want to create a trigger when I key in the details of BookingDetails it will automatically update totaldays inside the Booking table. Below is my code:
BookingDetails:
create table BookingDetail (
BD_ID int primary key not null,
Date_In date,
Date_Out date,
BK_ID int,
Room_ID int,
foreign key(BK_ID) references Booking(BK_ID),
foreign key(Room_ID) references Room(Room_ID)
)
And also Booking
create table Booking (
BK_ID int primary key not null,
BK_Date Date,
BK_TotalDays int,
BK_PayStatus char(6),
Cus_ID int,
Emp_ID int,
foreign key(Cus_ID) references customer(Cus_ID),
foreign key(Emp_ID) references Employee(Emp_ID)
)
With the function and trigger created:
create function countdays(t1 date, t2 date)
returns INT
return (timestampdiff(16, char(timestamp(t2) - timestamp(t1))))
create trigger totaldays
after insert on bookingdetail
referencing new as n
for each row mode db2sql
update booking
set bk_totaldays =
countdays((select date_in from bookingdetail), (select date_out from
bookingdetail))
where booking.bk_id = n.bk_id;
I have no problem executing these syntax, but when I try to input a new record inside Booking Detail to let the trigger triggers in Booking, errors occured, may I ask why? Thanks in advance.
Look at the information provided by the SQL error:
db2 ? SQL0811
SQL0811N The result of a scalar fullselect, SELECT INTO statement, or
VALUES INTO statement is more than one row.
So this part of your trigger expressions returns more than 1 row
set bk_totaldays = countdays((select date_in from bookingdetail),
(select date_out from bookingdetail))
Fix this to return a single row.
#MichaleTiefenbacher is correct about the cause of the error, but he's wrong about what to do about it. Let's take another look at your trigger again:
create trigger totaldays
after insert on bookingdetail
referencing new as n -- wait, what's this?
for each row mode db2sql
update booking
set bk_totaldays =
countdays((select date_in from bookingdetail), (select date_out from bookingdetail))
where booking.bk_id = n.bk_id;
You have a reference to the value you just inserted! When using FOR EACH ROW, the table reference NEW refers to the singular row just inserted. So you don't even need to look at the full table, just use what you just worked with:
create trigger totaldays
after insert on bookingdetail
referencing new as n
for each row mode db2sql
update booking
set bk_totaldays = countdays(n.date_in, n.date_out)
where booking.bk_id = n.bk_id;
(As I mentioned in my comment to your question, you'll likely want to change how you calculate the days, but that's irrelevant for this)

Ensure endDate equal to or later than startDate

I have a table in which I need to ensure that startDate is not later than endDate.
Either date can be updated so the rules have to apply regardless if startDate or endDate (or both) are entered/modified.
Is it better to use triggers, stored procedures or something else? Any sample syntax is appreciated.
Use a check constraint. Much simpler than a trigger.
CREATE TABLE dbo.foo
(
StartDate DATE NOT NULL,
EndDate DATE NOT NULL,
CONSTRAINT CheckEndLaterThanStart CHECK (EndDate >= StartDate)
);
If the table already exists:
ALTER TABLE dbo.foo
ADD CONSTRAINT CheckEndLaterThanStart
CHECK (EndDate >= StartDate);
If you try to insert a start date later than the end date, you'll get:
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the CHECK constraint "CheckEndLaterThanStart". The conflict occurred in database "AdventureWorks2012", table "dbo.foo".
The statement has been terminated.