check constraint not working as per the condition defined in sql server - sql

I have a table aa(id int, sdate date, edate date, constraint chk check(sdate<= enddate). For a particular id I have to check for overlapping dates. That is I do not want any one to insert data of a perticular id which has overlapping dates. So i need to check the below conditions -
if #id = id and (#sdate >= edate or #edate <= sdate) then allow insert
if #id = id and (#sdate < edate or #edate > sdate) then do not allow insert
if #id <> id then allow inserts
I have encapsulated the above logic in a function and used that function in check constraint. Function is working fine but check constraint is not allowing me to enter any records. I do not know why - my function and constraint are mentioned below :
alter function fn_aa(#id int,#sdate date,#edate date)
returns int
as
begin
declare #i int
if exists (select * from aa where id = #id and (#sdate >= edate or #edate <= sdate)) or not exists(select * from aa where id = #id)
begin
set #i = 1
end
if exists(select * from aa where id = #id and (#sdate < edate or #edate < sdate))
begin
set #i = 0
end
return #i
end
go
alter table aa
add constraint aa_ck check(dbo.fn_aa(id,sdate,edate) = 1)
Now when I try to insert any value in the table aa I get the following error -
"Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the CHECK constraint "aa_ck". The conflict occurred in database "tempdb", table "dbo.aa".
The statement has been terminated."
Function is returning value 1 but constraint is not allowing to insert data. Can some one help me here. I am trying for last 2 hours but cannot understand what am i doing wrong?
-

I think your logic is wrong. Two rows overlap
alter function fn_aa(#id int,#sdate date,#edate date)
returns int
as
begin
if exists (select *
from aa
where id = #id and
#sdate < edate and #edate > sdate
)
begin
return 0;
end;
return 1;
end;
Your version would return 1 when either of these conditions is true: #sdate >= edate or #edate <= sdate. However, checking for an overlap depends on both end points.

Related

While Loop SQL not populating complete results

Question: the iteration happens only till record 131 and gives accurate value, after that the parameter #ADE_END_DATE returns a NULL value, why would that be? Below is my code.
Additionally I noticed the column Leave_Date has NULL values and the iteration stops and returns NULL value for the parameter #ADE_END_DATE where the NULL value starts.
Thanks for your help.
BEGIN
DECLARE #HIREDATEPlus1Yr DATETIME
DECLARE #ADE_Leave_Date DATETIME
DECLARE #ADE_End_Date DATETIME
DECLARE #ADE_Start_Date DATETIME
DECLARE #DATECAL DATETIME
DECLARE #i INT
DECLARE #j INT
DECLARE #Loop_length INT
DECLARE #ID VARCHAR(18)
-- start of loop
SET #j = 1
-- Loop length will equal to the list of all ADRs
SET #Loop_Length = (SELECT COUNT([AD_ID])
FROM [DS_ADHOC_MOPs].[ADE].[List]
WHERE Status NOT IN ('MANAGER', 'TBH', 'FROZEN'))
-- Loop through each ADRs
WHILE (#j <= #Loop_length)
BEGIN
-- Loop through each ADRs
SET #i = 0
-- Find AD ID
SET #ID = (SELECT TOP 1 [AD_ID] FROM [DS_ADHOC_MOPs].[ADE].[List]
WHERE [AD_ID] NOT IN (SELECT TOP (#j-1) [AD_ID]
FROM [DS_ADHOC_MOPs].[ADE].[List]
WHERE ([AD_ID] IS NOT NULL
AND Status NOT IN ('MANAGER', 'TBH', 'FROZEN'))))
-- Find the start date of the ADR
SET #ADE_Start_Date = (SELECT TOP 1 [Hire_Date]
FROM [DS_ADHOC_MOPs].[ADE].[List]
WHERE [AD_ID] NOT IN (SELECT TOP (#j-1) [AD_ID]
FROM [DS_ADHOC_MOPs].[ADE].[List]
WHERE ([AD_ID] IS NOT NULL
AND Status NOT IN ('MANAGER', 'TBH', 'FROZEN'))))
-- Hire date plus 1 year
SET #HIREDATEPlus1Yr = DATEADD(YEAR, 1, #ADE_Start_Date)
--Adding Leave Date
SET #ADE_Leave_Date = (SELECT TOP 1 [LEAVE_DATE]
FROM [DS_ADHOC_MOPs].[ADE].[List]
WHERE [AD_ID] NOT IN (SELECT TOP (#j-1) [AD_ID]
FROM [DS_ADHOC_MOPs].[ADE].[List]
WHERE ([AD_ID] IS NOT NULL
AND Status NOT IN ('MANAGER', 'TBH', 'FROZEN'))))
-- Set a temporary variable which will be 1 year from now. Use the Date ADD formulae to start date, if they are leaver before one year then add leave date (Use IF): DONE
-- Put everything inside the while loop and add opportunity selecting to it.
IF #ADE_Leave_Date IS NULL
SET #ADE_End_Date = DATEADD(YEAR, 1, #ADE_Start_Date)
ELSE IF #HIREDATEPlus1Yr < #ADE_Leave_Date
SET #ADE_End_Date = DATEADD(YEAR, 1, #ADE_Start_Date)
ELSE
SET #ADE_End_Date = #ADE_Leave_Date
SET #DATECAL = datediff(DAY, #ADE_Start_Date, #ADE_End_Date)
SET #j = #j + 1
UPDATE #TEMPTABLEEEE
SET [#ADE_End_Date] = #ADE_End_Date
WHERE #ID = AD_ID
END
SELECT * FROM #TEMPTABLEEEE
END
I'm not sure why you are using a WHILE loop. It looks like this code could be much simplified. SQL is a set based language. Whenever possible, you should try to handle your data as a whole set instead of breaking it down into row by row evaluations.
Does this give you what you need? If the table has more than one row for each AD_ID, you will need to get the MAX() or MIN() Hire_Date/LEAVE_DATE. To improve the answer, consider providing sample data.
UPDATE t
SET [#ADE_End_Date] = ed.ADE_EndDate
FROM #TEMPTABLEEEE AS t
INNER JOIN (
SELECT AD_ID
,CASE
WHEN LEAVE_DATE IS NULL THEN DATEADD(YEAR,1,Hire_Date)
WHEN DATEADD(YEAR,1,Hire_Date) < LEAVE_DATE THEN DATEADD(YEAR,1,Hire_Date)
ELSE LEAVE_DATE
END AS ADE_EndDate
FROM DS_ADHOC_MOPs.ADE.List
WHERE Status NOT IN ('MANAGER', 'TBH', 'FROZEN')
) AS ed ON t.AD_ID = ed.AD_ID

New to SQL - Why is my Insert into trying to insert NULL into primary key?

What I want to do is insert a range of dates into multiple rows for customerID=1. I have and insert for dbo.Customer(Dates), specifying my that I want to insert a record into the Dates column for my Customer table, right? I am getting error:
Cannot insert the value NULL into column 'CustomerId', table 'dbo.Customers'
Sorry if I am way off track here. I have looked at similar threads to find out what I am missing, but I'm not piecing this together. I am thinking it wants to overwrite the existing customer ID as NULL, but I am unsure why exactly since I'm specifying dbo.Customer(Dates) and not the existing customerID for that record.
declare #date_Start datetime = '03/01/2011'
declare #date_End datetime = '10/30/2011'
declare #date datetime = #date_Start
while #date <= #date_End
begin
insert into dbo.Customer(Dates) select #date
if DATEPART(dd,#date) = 0
set #date = DATEADD(dd, -1, DATEADD(mm,1,#date))
else
set #date = DATEADD(dd,1,#date)
end
select * from dbo.Customer
The primary key is customerId, but you are not inserting a value.
My guess is that you declared it as a primary key with something like this:
customerId int primary key,
You want it to be an identity column, so the database assigns a value:
customerId int identity(1, 1) primary key
Then, you don't need to assign a value into the column when you insert a new row -- the database does it for you.
Your Customer table has a column named CustomerId and which column is NOT Nullable so you have to provide that column value as well. If your column type is Int try the bellow code:
declare #date_Start datetime = '03/01/2011'
declare #date_End datetime = '10/30/2011'
declare #date datetime = #date_Start
DECLARE #cusId INT
SET #cusId = 1
while #date <= #date_End
begin
insert into dbo.Customer(CustomerId, Dates) select #cusId, #date
if DATEPART(dd,#date) = 0
set #date = DATEADD(dd, -1, DATEADD(mm,1,#date))
else
set #date = DATEADD(dd,1,#date)
SET #cusId = #cusId + 1;
end
select * from dbo.Customer
thank you for the feedback. I think I'm scrapping this and going to go with creating a separate table to JOIN. Not sure why I didn't start doing that before

How can this SQL check constraint for time ranges fail, when table is empty?

I have implemented a time range validation, as a check constraint, using a function in SQL, using this guide, almost to the letter.
Creating the function first:
create function dbo.ValidateStatusPeriodInfoTimeRange
(
#btf_id VARCHAR(32),
#start_time BIGINT,
#end_time BIGINT
)
returns bit
as
begin
declare #Valid bit = 1;
if exists( select *
from dbo.StatusPeriodInfoOccurrence o
where o.btf_id = #btf_id
and #start_time <= o.end_time and o.start_time <= #end_time )
set #Valid = 0;
return #Valid;
end
And then the constraint, using the function:
alter table dbo.StatusPeriodInfoOccurrence with nocheck add constraint
CK_StatusPeriodInfoOccurrence_ValidateTimeRange
check (dbo.ValidateStatusPeriodInfoTimeRange(btf_id, start_time, end_time) = 1);
When I try to insert an element into a completely empty table, I get:
The INSERT statement conflicted with the CHECK constraint
"CK_StatusPeriodInfoOccurrence_ValidateTimeRange". The conflict occurred in
database "D600600TD01_BSM_Surveillance", table "dbo.StatusPeriodInfoOccurrence".
I tried to figure out if I did something wrong in the function itself, and created this query to check it's return value:
DECLARE #ReturnValue INT
EXEC #ReturnValue = ValidateStatusPeriodInfoTimeRange
#btf_id = 'a596933eff9143bceda5fc5d269827cd',
#start_time = 2432432,
#end_time = 432432423
SELECT #ReturnValue
But this returns 1, as it should.
I am at a loss on how to continue debugging this. All parts seem to work, but the whole does not. Any ideas on how the insert statement can conflict with the check constraint?
Edit: Here is my insert statement for completion:
INSERT INTO StatusPeriodInfoOccurrence (btf_id, start_time, end_time) VALUES ('a596933eff9143bceda5fc5d269827cd',2432432,432432423);
There is an additional primary key comlumn with identity auto increment.
CHECK constraints happen after the row is inserted, so in its current form, the constraint fails because the very row that was inserted matches the constraint. In order for this to work as a constraint (not a trigger) there must be a way to distinguish the row we're checking from all other rows. MichaƂ's answer shows how to do this without relying on an IDENTITY, but if you do have that explicitly excluding the row may be simpler:
create function dbo.ValidateStatusPeriodInfoTimeRange
(
#id INT,
#btf_id VARCHAR(32),
#start_time BIGINT,
#end_time BIGINT
)
returns bit
as
begin
declare #Valid bit = 1;
if exists( select *
from dbo.StatusPeriodInfoOccurrence o
where o.id <> #id AND o.btf_id = #btf_id
and #start_time <= o.end_time and o.start_time <= #end_time )
set #Valid = 0;
return #Valid;
end;
with the constraint defined as
check (dbo.ValidateStatusPeriodInfoTimeRange(id, btf_id, start_time, end_time) = 1)
Regardless of the approach, indexes on (btf_id, start_time) and (btf_id, end_time) are a good idea to keep this scalable, otherwise a full table scan is necessary on every insert.
As was mentioned in comments, constraint is checked after the record is inserted into a table, then the transaction is commited or rolled back, depending on result of a check, which in your example will always fails, as query:
select *
from dbo.StatusPeriodInfoOccurrence o
where o.btf_id = #btf_id
and #start_time <= o.end_time and o.start_time <= #end_time
will return always at least one row (the one being inserted).
So, knowing that, you should check if the query returns more than one record, so the condition in if statement should become:
if (select count(*)
from dbo.StatusPeriodInfoOccurrence o
where o.btf_id = #btf_id
and #start_time <= o.end_time and o.start_time <= #end_time ) > 1
This solution works fine (tested on my DB).

SQL Server 2005 - writing an insert and update trigger for validation

I'm pretty bad at SQL, so I need someone to check my trigger query and tell me if it solves the problem and how acceptable it is. The requirements are a bit convoluted, so please bear with me.
Suppose I have a table declared like this:
CREATE TABLE Documents
(
id int identity primary key,
number1 nvarchar(32),
date1 datetime,
number2 nvarchar(32),
date2 datetime
);
For this table, the following constraints must be observed:
At least one of the number-date pairs should be filled (both the number and the date field not null).
If both number1 and date1 are not null, a record is uniquely identified by this pair. There cannot be two records with the same number1 and date1 if both fields are not null.
If either number1 or date1 is null, a record is uniquely identified by the number2-date2 pair.
Yes, there is a problem of poor normalization, but I cannot do anything about that.
As far as I know, I cannot write unique indexes on the number-date pairs that check whether some of the values are null in SQL Server 2005. Thus, I tried validating the constraints with a trigger.
One last requirement - the trigger should have no inserts of its own, only validation checks. Here's what I came up with:
CREATE TRIGGER validate_requisite_uniqueness
ON [Documents]
FOR INSERT, UPDATE
AS
BEGIN
DECLARE #NUMBER1 NVARCHAR (32)
DECLARE #DATE1 DATETIME
DECLARE #NUMBER2 NVARCHAR (32)
DECLARE #DATE2 DATETIME
DECLARE #DATETEXT VARCHAR(10)
DECLARE inserted_cursor CURSOR FAST_FORWARD FOR SELECT number1, date1, number2, date2 FROM Inserted
IF NOT EXISTS (SELECT * FROM INSERTED)
RETURN
OPEN inserted_cursor
FETCH NEXT FROM inserted_cursor into #NUMBER1, #DATE1, #NUMBER2, #DATE2
WHILE ##FETCH_STATUS = 0
BEGIN
IF (#NUMBER1 IS NULL OR #DATE1 IS NULL)
BEGIN
IF (#NUMBER2 IS NULL OR #DATE2 IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second number-date pair should be filled.', 10, 1)## Heading ##
END
END
IF (#NUMBER1 IS NOT NULL AND #DATE1 IS NOT NULL)
BEGIN
IF ((SELECT COUNT(*) FROM Documents WHERE number1 = #NUMBER1 AND date1 = #DATE1) > 1)
BEGIN
ROLLBACK TRANSACTION
SET #DATETEXT = CONVERT(VARCHAR(10), #DATE1, 104)
RAISERROR ('A document with the number1 ''%s'' and date1 ''%s'' already exists.', 10, 1, #NUMBER1, #DATETEXT)
END
END
ELSE IF (#NUMBER2 IS NOT NULL AND #DATE2 IS NOT NULL) /*the DATE2 check is redundant*/
BEGIN
IF ((SELECT COUNT(*) FROM Documents WHERE number2 = #NUMBER2 AND date2 = #DATE2) > 1)
BEGIN
ROLLBACK TRANSACTION
SET #DATETEXT = CONVERT(VARCHAR(10), #DATE2, 104)
RAISERROR ('A document with the number2 ''%s'' and date2 ''%s'' already exists.', 10, 1, #NUMBER2, #DATETEXT)
END
END
FETCH NEXT FROM inserted_cursor
END
CLOSE inserted_cursor
DEALLOCATE inserted_cursor
END
Please tell me how well-written and efficient this solution is.
A couple of questions I can come up with:
Will this trigger validate correctly against existing rows and newly inserted/updated rows in case of bulk modifications? It should, because the modifications are already applied to the table in the scope of this transaction, right?
Are the constraint violations handled correctly? Meaning, was I right to use the rollback transaction and raiserror pair?
Is the "IF NOT EXISTS (SELECT * FROM INSERTED) RETURN" statement used correctly?
Is the use of COUNT to check the constraints acceptable, or should I use some other way of checking the uniqueness of number-date pairs?
Can this solution be optimized in terms of execution speed? Should I add non-unique indexes on both number-date pairs?
Thanks.
EDIT:
A solution using a check constraint and an indexed view, based on Damien_The_Unbeliever's answer:
CREATE TABLE dbo.Documents
(
id int identity primary key,
number1 nvarchar(32),
date1 datetime,
number2 nvarchar(32),
date2 datetime,
constraint CK_Documents_AtLestOneNotNull CHECK (
(number1 is not null and date1 is not null) or
(number2 is not null and date2 is not null)
)
);
go
create view dbo.UniqueDocuments
with schemabinding
as
select
CASE WHEN (number1 is not null and date1 is not null)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END as first_pair_filled,
CASE WHEN (number1 is not null and date1 is not null)
THEN number1
ELSE number2
END as number,
CASE WHEN (number1 is not null and date1 is not null)
THEN date1
ELSE date2
END as [date]
from
dbo.Documents
go
create unique clustered index IX_UniqueDocuments on dbo.UniqueDocuments(first_pair_filled,number,[date])
go
I would avoid the trigger, and use a check constraint and an indexed view:
CREATE TABLE dbo.Documents
(
id int identity primary key,
number1 nvarchar(32),
date1 datetime,
number2 nvarchar(32),
date2 datetime,
constraint CK_Documents_AtLestOneNotNull CHECK (
(number1 is not null and date1 is not null) or
(number2 is not null and date2 is not null)
)
);
go
create view dbo.UniqueDocuments
with schemabinding
as
select
COALESCE(number1,number2) as number,
COALESCE(date1,date2) as [date]
from
dbo.Documents
go
create unique clustered index IX_UniqueDocuments on dbo.UniqueDocuments(number,[date])
go
Which has the advantage that, although there is some "trigger-like" behaviour because of the indexed view, it's well-tested code that's already been deeply integrated into SQL Server.
I would use this logic instead (I didn't type it all as it takes ages), and definitely use SELECT 1 FROM ... in the IF EXISTS() statement as it helps performance. Also remove the cursors like marc_s said.
CREATE TRIGGER trg_validate_requisite_uniqueness
ON dbo.[Documents]
AFTER INSERT, UPDATE
AS
DECLARE #Number1 NVARCHAR(100) = (SELECT TOP 1 number1 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #Date1 DATETIME = (SELECT TOP 1 date1 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #Number2 NVARCHAR(100) = (SELECT TOP 1 number2 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #Date2 DATETIME = (SELECT TOP 1 date2 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #DateText NVARCHAR(100)
IF EXISTS (SELECT 1 FROM dbo.Documents AS D
INNER JOIN INSERTED AS I ON D.id = I.id WHERE I.Number1 IS NULL AND I.number2 IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second number pair should be filled.', 10, 1)
END
ELSE IF EXISTS (SELECT 1 FROM dbo.Documents AS D
INNER JOIN INSERTED AS I ON D.id = I.id WHERE I.Date1 IS NULL AND I.Date2 IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second date pair should be filled.', 10, 1)
END
ELSE IF EXISTS (SELECT 1 FROM dbo.Documents AS D
GROUP BY D.number1, D.date1 HAVING COUNT(*) >1
)
BEGIN
ROLLBACK TRANSACTION
SET #DateText = (SELECT CONVERT(VARCHAR(10), #Date1, 104))
RAISERROR ('Cannot have duplicate values', 10, 1, #Number1, #DateText )
END

SQL query for for finding range between two numbers, to stop duplicates to enter

I have a data in a table with two columns from and to, for example: 5-9.
If I insert a new row 1-6, it should NOT be accepted, as 6 lies in between 5 and 9.
1-3 - should accept (no overlap), 10-13 - should also accept.
I need a single query for this
Try this:
if not exist (select * from table where #from between [from] and [to])
begin
if not exist (select * from table where #to between [from] and [to])
begin
if not exist(select * from table where [from] between #from and #to)
begin
insert into table
select #from, #to
end
end
end
Depending on SQL vendor, implementation will differ, but idea is the same: you need to create a TRIGGER on INSERT or UPDATE.
This is a solution for PostgreSQL:
CREATE TABLE mytable (
id SERIAL,
m1 INTEGER,
m2 INTEGER
);
CREATE OR REPLACE FUNCTION mytable_fn()
RETURNS trigger AS
$body$
DECLARE cnt INTEGER;
BEGIN
SELECT count(*) INTO cnt
FROM mytable
WHERE numrange(NEW.m1, NEW.m2)
&& numrange( m1, m2);
IF cnt > 0 THEN RAISE EXCEPTION 'Constraint violation';
ELSE RETURN NEW;
END IF;
END;
$body$
LANGUAGE 'plpgsql';
CREATE TRIGGER mytable_tr
BEFORE INSERT OR UPDATE
ON mytable FOR EACH ROW
EXECUTE PROCEDURE mytable_fn();
Try this
DECLARE #from INT
DECLARE #to INT
SET #from = 14
SET #to = 16
IF NOT EXISTS (SELECT 1 FROM Table1 WHERE #from BETWEEN [from] AND [to]
OR #to BETWEEN [from] AND [to]
OR #from <= [from] AND #to >= [to])
BEGIN
INSERT INTO Table1([from], [to]) VALUES(#from, #to)
END
ELSE
BEGIN
--Already Exists
END