简体   繁体   English

Postgres 触发器检查与现有记录的日期重叠

[英]Postgres trigger to check date overlap with already existing records

I have a table which have 2 column, start date and end date.我有一个表,有 2 列,开始日期和结束日期。

  • 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.你可以看到我正在一个一个地测试所有的重叠条件,这个关于我使用 9999 年之前的日期解决的非结束时间段的问题,以使所有工作正常。

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这些检查很多是“手动”的,我想知道这是否可以通过使用 intersect 或类似的查询来实现,但我还没有找到可行的方法


EDIT Based on @GMB approach, this is the final result编辑基于@GMB 方法,这是最终结果

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.在我的实际场景中,我还有一列定义记录是否处于活动状态,因此我在定义中添加了 where 子句。 I also moved to a separate ADD CONSTRAINT statement because my table already exists, so I only add this one.我还移到了单独的ADD CONSTRAINT语句,因为我的表已经存在,所以我只添加了这个。

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. daterange()的参数[]使范围在两端都包含在内,这就是我理解您的问题的方式。


Edit : if you want the exclusion to be based on another column, say user_id :编辑:如果您希望排除基于另一列,请说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 &&
    )
);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM