I have a table which have 2 column, start date and end date.
- start date is required
- end date is optional (so it’s a period that basically never ends)
I’m creating a trigger which ensure that no record overlap with the other ones, and so far I made this
DROP TABLE IF EXISTS public.working_hours; CREATE TABLE public.working_hours ( id serial NOT NULL, date_start date NOT NULL, date_end date NULL ); -- Insert some valid data INSERT INTO public.working_hours (date_start,date_end) VALUES ('2020-12-10'::date,'2020-12-20'::date); -- Setup trigger to check create or REPLACE FUNCTION check_date_overlap() RETURNS trigger as $body$ declare temprow record; a date; b date; c date; d date; begin c := new.date_start; d := new.date_end; if d is null -- End date is optional then d := '9999-12-31'::date; end if; for temprow in SELECT * FROM public.working_hours WHERE id != new.id -- Avoid the record itself which is under update ORDER BY date_start loop a := temprow.date_start; b := temprow.date_end; if b is null -- End date is optional then b := '9999-12-31'::date; end if; /* * temprow: A-------------B * new: C----D */ if a < c and b > d then RAISE EXCEPTION 'case A: record is overlapping with record %', temprow.id; end if; /* * temprow: A----B * new: C-------------D */ if a > c and b < d then RAISE EXCEPTION 'case B: record is overlapping with record %', temprow.id; end if; /* * tn: A-------------B * new: C-------------D */ if a < c and c < b and b < d then RAISE EXCEPTION 'case C: record is overlapping with record %', temprow.id; end if; /* * temprow: A-------------B * new: C-------------D */ if c < a and a < d and d < b then RAISE EXCEPTION 'case D: record is overlapping with record %', temprow.id; end if; /* * temprow: A-------------B * new: C-------------D */ if c < a and a = d and d < b then RAISE EXCEPTION 'case E: record is overlapping with record %', temprow.id; end if; /* * temprow: A-------------B * new: C-------------D */ if a < c and b = c and b < d then RAISE EXCEPTION 'case F: record is overlapping with record %', temprow.id; end if; end loop; RETURN NEW; end $body$ LANGUAGE plpgsql; drop trigger if exists on_check_date_overlap on public.working_hours; create trigger on_check_date_overlap before update or insert on public.working_hours for each row execute procedure check_date_overlap(); -- Test case A fail -- INSERT INTO public.working_hours (date_start,date_end) VALUES ('2020-12-15','2020-12-18'); -- Test case B fail -- INSERT INTO public.working_hours (date_start,date_end) VALUES ('2020-12-5','2020-12-25'); -- Test case C fail -- INSERT INTO public.working_hours (date_start,date_end) VALUES ('2020-12-15','2020-12-25'); -- Test case D fail -- INSERT INTO public.working_hours (date_start,date_end) VALUES ('2020-12-5','2020-12-15'); -- Test case E fail -- INSERT INTO public.working_hours (date_start,date_end) VALUES ('2020-12-5','2020-12-10'); -- Test case F fail -- INSERT INTO public.working_hours (date_start,date_end) VALUES ('2020-12-20','2020-12-25'); -- Test success -- INSERT INTO public.working_hours (date_start,date_end) VALUES ('2020-12-21','2020-12-25');
You can see that I’m testing all the overlap conditions one by one, the problem about a non-ending period I solved using a date up to year 9999, to make all working.
This code I’m sharing it’s working, at the end of it you cand find a insert statement which ends with failure (related to the given case),
Those checks are a lot “manually”, I’m wondering if this can be achieved with a query which uses intersect or similar but I haven’t find a working approach
EDIT Based on @GMB approach, this is the final result
DROP TABLE IF EXISTS public.working_hours; CREATE TABLE public.working_hours ( id serial NOT NULL, status varchar(255) not null, user_id int NOT null, date_start date NOT NULL, date_end date NULL ); alter table public.working_hours ADD CONSTRAINT prevent_overlap EXCLUDE USING gist (user_id WITH =, daterange(date_start, coalesce(date_end, 'infinity'), '[]') WITH &&) where (status = 'active') ; -- Insert some valid data INSERT INTO public.working_hours (status, user_id, date_start,date_end) VALUES ('active', 1, '2020-12-10'::date,'2020-12-20'::date); INSERT INTO public.working_hours (status, user_id, date_start,date_end) VALUES ('deleted', 1, '2020-12-5'::date,'2020-12-15'::date); INSERT INTO public.working_hours (status, user_id, date_start,date_end) VALUES ('active', 2, '2020-12-10'::date,'2020-12-20'::date); -- Updating from deleted to active will fail update public.working_hours set status = 'active' where id = 2;
In my actual scenario I also have a column which defines if the record is active or not, so I added a where clause in the definition. I also moved to a separate ADD CONSTRAINT
statement because my table already exists, so I only add this one.
Advertisement
Answer
No need for complicated trigger code. You can do what you want simplify and efficiently with an exclusion constraint:
CREATE TABLE public.working_hours ( id serial NOT NULL, date_start date NOT NULL, date_end date NULL, EXCLUDE USING gist (daterange(date_start, coalesce(date_end, 'infinity'), '[]') WITH &&) );
Argument []
to daterange()
makes the range inclusive on both ends, which is how I understood your question.
Edit: if you want the exclusion to be based on another column, say user_id
:
CREATE TABLE public.working_hours ( id serial NOT NULL, user_id int NOT NULL date_start date NOT NULL, date_end date NULL, EXCLUDE USING gist ( user_id WITH =, daterange(date_start, coalesce(date_end, 'infinity'), '[]') WITH && ) );