Postgres 触发器检查日期是否与现有记录重叠
Postgres trigger to check date overlap with already existing records
我有一个 table,它有 2 列,开始日期和结束日期。
- 需要开始日期
- 结束日期是可选的(所以这是一个基本上永远不会结束的时期)
我正在创建一个触发器,以确保没有记录与其他记录重叠,到目前为止我已经做到了这一点
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');
你可以看到我正在一个一个地测试所有的重叠条件,关于一个非结束期的问题,我使用一个直到 9999 年的日期来解决,使所有工作正常。
我分享的这段代码是有效的,在它的末尾你可以找到一个以失败结尾的插入语句(与给定的案例相关),
这些检查很多都是“手动”进行的,我想知道这是否可以通过使用相交或类似查询来实现,但我还没有找到可行的方法
编辑 基于@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;
在我的实际场景中,我还有一列定义记录是否处于活动状态,因此我在定义中添加了一个where 子句。我也移动到一个单独的 ADD CONSTRAINT
语句,因为我的 table 已经存在,所以我只添加这个
无需复杂的触发代码。您可以使用排除约束来简化和高效地做您想做的事情:
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 &&)
);
参数 []
到 daterange()
使两端都包含范围,这就是我对你的问题的理解。
编辑:如果您希望排除基于另一列,请说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 &&
)
);
我有一个 table,它有 2 列,开始日期和结束日期。
- 需要开始日期
- 结束日期是可选的(所以这是一个基本上永远不会结束的时期)
我正在创建一个触发器,以确保没有记录与其他记录重叠,到目前为止我已经做到了这一点
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');
你可以看到我正在一个一个地测试所有的重叠条件,关于一个非结束期的问题,我使用一个直到 9999 年的日期来解决,使所有工作正常。
我分享的这段代码是有效的,在它的末尾你可以找到一个以失败结尾的插入语句(与给定的案例相关),
这些检查很多都是“手动”进行的,我想知道这是否可以通过使用相交或类似查询来实现,但我还没有找到可行的方法
编辑 基于@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;
在我的实际场景中,我还有一列定义记录是否处于活动状态,因此我在定义中添加了一个where 子句。我也移动到一个单独的 ADD CONSTRAINT
语句,因为我的 table 已经存在,所以我只添加这个
无需复杂的触发代码。您可以使用排除约束来简化和高效地做您想做的事情:
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 &&)
);
参数 []
到 daterange()
使两端都包含范围,这就是我对你的问题的理解。
编辑:如果您希望排除基于另一列,请说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 &&
)
);