Here is the table
CREATE TABLE date_ranges (
id SERIAL PRIMARY KEY,
user_id INTEGER,
start_at DATE,
end_at DATE,
is_exception BOOLEAN
);
ALTER TABLE date_ranges ADD CONSTRAINT date_ranges_bounds
EXCLUDE USING gist (user_id WITH =, daterange(start_at, end_at, '[]') WITH &&)
WHERE (NOT is_exception);
I have a table with working ranges and here is a flag is_exception
which indicates that this range is an exception from the regular range and I want to make a constraint that all regular ranges do not overlap each other and exception ranges belong to the regular range.
I tried to use <@
operator but have found that it is not commutative.
ALTER TABLE date_ranges ADD CONSTRAINT date_ranges_bounds
EXCLUDE USING gist (user_id WITH =, daterange(start_at, end_at, '[]') WITH <@)
WHERE (is_exception);
Is there any option to do this? Shall I create custom operator for this case?
UPDATE
Test cases:
user_id start_at end_at is_exception is_day_off
1 2019-03-01 2019-03-05 FALSE FALSE // is fine
1 2019-03-01 2019-03-05 FALSE FALSE // FAIL. Range intersects with range above
1 2019-03-02 2019-03-03 TRUE FALSE // is fine since exception is inside "regular range above"
1 2019-03-04 2019-03-04 TRUE FALSE // is fine since exception is inside "regular range above" & does not intersect another exception
1 2019-03-03 2019-03-03 FALSE TRUE // is fine since day off is inside "regular range above"
1 2019-03-05 2019-03-05 FALSE TRUE // is fine since day off is inside "regular range above" & does not intersect another day off
1 2019-03-02 2019-03-03 TRUE FALSE // FAIL. Exception should NOT intersect with another one
1 2019-03-03 2019-03-03 FALSE TRUE // FAIL. Day off should NOT intersect with another one
1 2019-03-15 2019-03-15 TRUE FALSE // FAIL. Exception is NOT inside "regular range above"
1 2019-03-10 2019-03-10 FALSE TRUE // FAIL. Day off is NOT inside "regular range above"
Best Answer
The (not overlapping) constraint between "regular" ranges can be enforced with an
EXCLUDE
constraint, as you it defined in the question.The (not overlapping) constraint between "exception" ranges can be enforced in the same manner and so can the constraint between "day off" ranges.
What is not trivial is the constraint between "regular" and "exception" ranges (and between "regular" and "day off" ranges):
This is not commutative, so it cannot be done with
EXCLUDE
constraints. What you really need is aFOREIGN KEY
constraint, a slightly more complicated one than a common foreign key constraint.As an example, a foreign key constraint defined n table
B
:enforces that:
which is quite similar to what you have. The problematic difference is that foreign key constraints can use only the equality (
=
) and not any other operator. To solve this problem, we'll have to duplicate the columns that need to be checked with the different operator ("is contained by",<@
).As a result, we'll need to split the table into two, "regular" and "exception" ranges (lets forget the "day off" for a bit) and apply our solution:
For the "regular" ranges table, we'll only need an additional
UNIQUE
constraint, for the foreign key to be defined (and we can skip theis_exception
andis_day_off
columns):For the "exception" ranges table, we'll need the
FOREIGN KEY
constraint and aCHECK
constraint for the `contained" operator:For the "day off", since we identified that are similar to "exception" ranges, we can either add another table (with identical structure as the
date_exception_ranges
) or keep only that and add a boolean column to identify whether it is an "exception" or a "day off".(The following assume that "exceptions" can overlap "day off" ranges. If they cannot, it's actually much simpler):
And finally, the above tested at dbfiddle.uk