August 4, 2006 at 5:00 pm
I am attempting to write an advanced trigger to validate a date range in my table.
Table X:
- id1 INT NOT NULL
- id2 INT NOT NULL
- start SMALLDATETIME NOT NULL
- stop SMALLDATETIME NULL
*NOTE THAT "end" is nullable!
I want to make certain that all inserted and updated records do not allow overlapping dates for records with the same id1 and id2.
This is EASY with single records. However, I am trying to use set-based operations to handle bulk inserts/updates (without using CURSORs).
What I want is:
Insert/Update all records that 1) do not already exist with id1 & id2 or 2) do not have overlapping dates.
Given a trigger...
CREATE TRIGGER dbo.x_validate ON x INSTEAD OF INSERT,UPDATE AS
I can retrieve *invalid* sets using:
SELECT x.*
FROM inserted new
INNER JOIN x ON
new.id1 = x.id1 AND
new.id2 = x.id2 AND
(
new.start BETWEEN x.start AND x.stop OR
(
new.stop IS NOT NULL AND
new.stop BETWEEN x.start AND x.stop
)
)
However, I want all VALID sets.
Related, but off-topic: This table has additional constraints to ensure that id1 <> id2 and stop > start.
August 4, 2006 at 9:44 pm
Eric, your current code misses an overlay... see line 8 below...
First of all, let's draw a picture of everything that has a start date less than the end date and the end date is not null. Line 01 represents the existing record... the other lines represent what a new record could have IF THERE'S A DATE RANGE OVERLAP...
01 S---------E (Existing record)
02 A---------B (possible new rec ranges from here down)
03 | A-----|---B
04 A---|-----B |
05 A---------B |
06 | A---------B
07 | A--B |
08 A---------|---------|---------B (Your code misses this one)
Now, let's figure this out... what simple conditions exist where we can easily identify that all the new record attempts in lines 02 through 08 have a date range (not working on the NULL end date thingy, yet... wait for it) that overlaps the existing record shown in line 01... You did better than most but, like I said, you missed the type of record in line 08. Here's the observation...
... All occurrences of "A" are ALWAYS <= "E"
and... All occurrences of "B" are ALWAYS >= "S"
and there are no exceptions. If the above conditions exist, then there must be a date range overlap between the new record and the old record.
A lot of people make the mistake of trying to find if the date range overlap exists when what you're really trying to find out is if the date range overlap doesn't exist...
With that in mind, this is true for those new records that don't have a date range overlap of the existing record...
01 S---------E (Existing record)
| |
09 A----B | |
10 | | A----B
... ANY occurrences of "A" are > "E"
OR ... ANY occurrences of "B" are < "S"
and there are no exceptions. If the above conditions exist, then there is NO date range overlap between the new record and the old record.
NOTICE that if "B" were to be a NULL on line 09, then it would cause the drawing to look like this...
01 S---------E (Existing record)
| |
09 A------------------------------------------>>>(null)
10 | | A----B
THAT'S kinda handy because a NULL can never equate to a value (unless you've made the horrible mistake of changing a server wide setting). So in the case of line 09 with NULL for "B", the condition of "B" < "S" would still work because "B" cannot be less than "S" if "B" is NULL because it can't be compared at all!
So, instead of you trying to find out if there is an overlap, take the easy road... try to find if there is NO overlap... so, in the same terms you wrote your good example in...
--===== Retrieve all "valid" rows that have no date range overlap with new recs
SELECT x.*
FROM Inserted new
INNER JOIN x
ON new.id1 = x.id1
AND new.id2 = x.id2
AND (
new.Start > x.Stop
OR
new.Stop < x.Start
)
Remember, the key is that because NULLS cannot be relationally compared, if new.Stop is NULL, the record will NOT qualify as "Valid".
{EDIT}
The same thing happens for if an end date is null in the existing records...
01 S------------------------->>>(null) (Existing record)
| |
09 A----B | |
10 | | A----B
One of the conditions is "A" > "E"... in this case, "E" is NULL and the comparison will not be able to be accomplished. This means the two records (original and line 10) have a date range overlap and will not be qualified in the SELECT I posted.
--Jeff Moden
Change is inevitable... Change for the better is not.
August 8, 2006 at 11:47 am
Here was the result I came up with... Jeff: I attempted to follow your notes and appreciated the test case examples. However, I did have difficulty translating the notes into a solution. Perhaps you could review the solution below and make some suggestions?
CREATE TABLE dt
(
dt_id INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
start SMALLDATETIME NOT NULL,
stop SMALLDATETIME NULL
);
GO
SET NOCOUNT ON;
INSERT INTO dt ( start, stop ) VALUES ( '1/1/2000', '12/31/2000 11:59' );
INSERT INTO dt ( start, stop ) VALUES ( '1/2/2001', '12/31/2001 11:59' );
INSERT INTO dt ( start, stop ) VALUES ( '1/1/2004', NULL ); --no end
GO
DECLARE @start SMALLDATETIME, @stop SMALLDATETIME;
DECLARE @last_stop SMALLDATETIME;
--test params
SELECT @start = '1/1/2003', @stop = '1/2/2005';
--max. smalldatetime value
SET @last_stop = CAST('June 6, 2079 11:59' AS SMALLDATETIME);
--use max value if null
IF @stop IS NULL
SET @stop = @last_stop;
IF EXISTS (
SELECT TOP 1 dt_id
FROM dt (fastfirstrow)
WHERE
--check if @start param is between start and stop dates
( ( @start > start ) AND ( @start < ISNULL(stop, @last_stop) ) ) OR
--check if @stop param is between start and stop dates
( ( @stop > start ) AND ( @stop < ISNULL(stop, @last_stop) ) ) OR
--check if start is between start and stop params
( ( start > @start ) AND ( start < @stop ) ) OR
--check if stop is between start and stop params
( ( ISNULL(stop, @last_stop) > @start ) AND ( ISNULL(stop, @last_stop) < @stop ) )
) PRINT 'yes';
ELSE PRINT 'no';
GO
DROP TABLE dt;
GO
August 8, 2006 at 8:48 pm
Dang... sorry Eric... got so tied up in the theory of it all, I forgot to demo how to do it set based...
Most of the SELECT list in the code below is just fluff to make the output pretty... the real key is in the JOIN and the WHERE clause. Contrary to what I said in my first post, I did go for the first option of finding where conflicts existed instead of where they do not.
I'm thinking that you may not want to use a trigger but you could with (what I think might be) simple mods to the code below. Sorry I can't spend a bit more time on this 'cause it's fun but I actually am getting ready for my once or twice a year, short term sabatical away from computers... I'm going on vacation sans any electronics. Just me, my fishing rod, and some cold beer!
Here's the full test setup and demonstration code of how to do it with many desired reservations against many actual reservations. The only shortcoming I see is that I don't check for conflicts against each other in the new batch of desired reservations... I did the tables as temp tables so I don't take a chance on dropping one of your permanent tables by mistake...
...and, yeah, I'm lazy so I took the easy way out on the date column formatting so that I could add words to a date column...
--===== Original problem definition posted by the user...
--"I want to make certain that all inserted and updated records do not allow
--overlapping dates for records with the same id1 and id2."
--===== If the experimental tables already exist, drop them
IF OBJECT_ID('TempDB..#Reserved') IS NOT NULL
DROP TABLE #Reserved
IF OBJECT_ID('TempDB..#Desired') IS NOT NULL
DROP TABLE #Desired
SET NOCOUNT ON
--===== This table simulates a table of known reserved dates by ID1/ID2
CREATE TABLE #Reserved
(
tmpReservedID INT IDENTITY(1, 1) PRIMARY KEY,
ID1 INT,
ID2 INT,
StartDate SMALLDATETIME NOT NULL,
StopDate SMALLDATETIME NULL
)
--===== This table simulates a table of desired reserved dates by ID1/ID2
CREATE TABLE #Desired
(
tmpDesiredID INT IDENTITY(1, 1) PRIMARY KEY,
ID1 INT,
ID2 INT,
StartDate SMALLDATETIME NOT NULL,
StopDate SMALLDATETIME NULL
)
--===== Populate the known reservation table with test data
INSERT INTO #Reserved (ID1,ID2,StartDate,StopDate)
SELECT 1,2,'1/1/2000', '12/31/2000 23:59' UNION ALL
SELECT 1,2,'1/2/2001', '12/31/2001 23:59' UNION ALL --1/1 is open
SELECT 1,2,'1/1/2004', NULL UNION ALL --No end
SELECT 3,4,'1/1/2000', '12/31/2000 23:59' UNION ALL
SELECT 3,4,'1/9/2001', '12/31/2001 23:59' UNION ALL --1/1 thru 1/8 are open
SELECT 3,4,'1/1/2004', '2/1/2004 23:59'
--===== Populate the desired reservation table with test data
INSERT INTO #Desired (ID1,ID2,StartDate,StopDate)
SELECT 1,2,'05/05/2000','05/05/2001' UNION ALL --Violates 2 existing reservations
SELECT 1,2,'04/04/2001','04/30/2001' UNION ALL --Violates 1 existing reservation
SELECT 1,2,'01/01/2005','01/01/2006' UNION ALL --Violates 1 existing reservation (tickles the null)
SELECT 1,2,'01/01/2001','01/01/2001 23:59' UNION ALL --Fits into the 1 day slot available
SELECT 1,2,'01/01/1999','12/31/1999 23:59' UNION ALL --Fits in before all the other reservations
SELECT 3,4,'02/02/2004',NULL UNION ALL --No conflict for extended stay
SELECT 3,4,'02/01/2004',NULL UNION ALL --Conflict by 1 day for extended stay
SELECT 3,4,'01/03/2001','01/08/2001 23:59' --Fits into 8 day slot with room at beginning
--===== Create a sample report to demo conflicting and non-conflicting dates
SELECT d.ID1,d.ID2,
DesiredStartDate = CONVERT(VARCHAR(30),d.StartDate,100),
DesiredStopDate = CASE
WHEN d.StopDate IS NULL
THEN 'Extended...'
ELSE CONVERT(VARCHAR(30),d.StopDate,100)
END,
Status = CASE
WHEN r.StartDate IS NULL
THEN 'No Conflict. OK to Book'
ELSE 'Conflict. DO NOT BOOK'
END,
ConflictingStartDate = ISNULL(CONVERT(VARCHAR(30),r.StartDate,100),''),
ConflictingStopDate = CASE
WHEN r.StartDate IS NOT NULL
AND r.StopDate IS NULL
THEN 'Extended...'
ELSE ISNULL(CONVERT(VARCHAR(30),r.StopDate,100),'')
END
FROM #Desired d
LEFT OUTER JOIN
#Reserved r
ON d.ID1 = r.ID1
AND d.ID2 = r.ID2
AND (d.StartDate <= r.StopDate OR r.StopDate IS NULL)
AND (d.StopDate >= r.StartDate OR d.StopDate IS NULL)
--===== Basic algorithm used from previous post
-- ... All occurrences of "A" are ALWAYS <= "E"
-- and... All occurrences of "B" are ALWAYS >= "S"
--Jeff Moden
Change is inevitable... Change for the better is not.
August 8, 2006 at 9:02 pm
p.s. Almost forgot... (pardon the soapbox, please) if you get into the habit of writing setbased code that will handle more than 1 row at a time, you've also solved the problem of handling just one row at a time in a set based fashion (that's a hint).
--Jeff Moden
Change is inevitable... Change for the better is not.
Viewing 5 posts - 1 through 4 (of 4 total)
You must be logged in to reply to this topic. Login to reply