Разрешить только рабочее время в таблице резервирования

Таблица резервирования PostgreSql 9.2 определяется как

CREATE EXTENSION btree_gist;
CREATE TABLE schedule (
  id serial primary key,
  during tsrange not null,
  EXCLUDE USING gist (during WITH &&)
);

Праздники указаны в таблице

CREATE TABLE holiday ( day primary key );

Время работы с 8 до 18:00 в рабочие дни, бронь возможна только с 30-минутным интервалом. Как добавить ограничения во время значений, чтобы они разрешали резервирование только во время работы:

  1. Даты начала и окончания в tsrange всегда одинаковы.
  2. Даты начала и окончания не могут быть субботой и воскресеньем
  3. Даты начала и окончания не могут отображаться в таблице государственных праздников.
  4. Время начала может быть только 8:00, 8:30, 9:00, 9:30, ... 16:00, 16:30, 17:00 или 17:30 включительно
  5. Время окончания может быть только 8:30, 9:00, 9:30, ... 16:00, 16:30, 17:00, 17:30 или 18:00 эксклюзивно

Разумно ли добавлять те или иные ограничения в эту таблицу? Если да, то как добавить? структура таблицы расписания может быть изменена, если это поможет.


person Andrus    schedule 25.11.2012    source источник
comment
Некоторые из них можно выполнить с помощью простых ограничений CHECK. Я не уверен, что вы можете реализовать отрицательное ограничение внешнего ключа непосредственно для государственных праздников; рассмотрите возможность использования триггера для этого.   -  person Craig Ringer    schedule 26.11.2012
comment
Я новичок в типе tsrange. Можете ли вы привести несколько примеров, особенно для временных ограничений?   -  person Andrus    schedule 26.11.2012


Ответы (2)


Вам нужно изменить определение таблицы и добавить некоторые ограничения check:

CREATE TABLE schedule (
  id serial primary key,
  during tsrange not null check(
    (lower(during)::date = upper(during)::date) and 
    (date_trunc('hour', upper(during)) + INTERVAL '30 min' * ROUND(date_part('minute', upper(during)) / 30.0) = upper(during)) and
    (date_trunc('hour', lower(during)) + INTERVAL '30 min' * ROUND(date_part('minute', lower(during)) / 30.0) = lower(during)) and
    (lower(during)::time >= '8:00'::time and upper(during)::time <= '18:00'::time) and
    (date_part('dow', lower(during)) in (1,2,3,4,5) and date_part('dow', upper(during)) in (1,2,3,4,5))
  ),
  EXCLUDE USING gist (during WITH &&)
);

Проверки идут в таком порядке

  • день начала и окончания совпадают
  • начало/конец может быть на 30-минутной границе
  • и между 8:00 .. 18:00
  • только будни

Нам нужно кое-что в таблице holiday: вставьте значения праздников ('2012-11-28');

check не может ссылаться на другую таблицу, поэтому нам нужна триггерная функция (может быть лучше поместить все проверки в эту функцию, т.е. иметь их в одном месте):

create function holiday_check() returns trigger language plpgsql stable as $$
begin
    if exists (select * from holiday where day in (lower(NEW.during)::date, upper(NEW.during)::date)) then
        raise exception 'public holiday';
    else
        return NEW;
    end if;
end;
$$;

Затем нам нужно создать триггеры до insert/update:

create trigger holiday_check_i before insert on schedule for each row execute procedure holiday_check();
create trigger holiday_check_u before update on schedule for each row execute procedure holiday_check();

Напоследок несколько тестов:

-- OK
insert into schedule(during) values (tsrange('2012-11-26 08:00', '2012-11-26 09:00'));
INSERT 0 1

-- out of business hours
insert into schedule(during) values (tsrange('2012-11-26 04:00', '2012-11-26 05:00'));
ERROR:  new row for relation "schedule" violates check constraint "schedule_during_check"
DETAIL:  Failing row contains (12, ["2012-11-26 04:00:00","2012-11-26 05:00:00")).

-- End time can be only 8:30, 9:00, 9:30, ... 16:00, 16:30, 17:00, 17:30 or 18:00 exclusive
insert into schedule(during) values (tsrange('2012-11-26 08:00', '2012-11-26 09:10'));
ERROR:  new row for relation "schedule" violates check constraint "schedule_during_check"
DETAIL:  Failing row contains (13, ["2012-11-26 08:00:00","2012-11-26 09:10:00")).

-- Start time can be only 8:00 , 8:30, 9:00, 9:30, ... 16:00, 16:30, 17:00 or 17:30 inclusive
insert into schedule(during) values (tsrange('2012-11-26 11:24', '2012-11-26 13:00'));
ERROR:  new row for relation "schedule" violates check constraint "schedule_during_check"
DETAIL:  Failing row contains (14, ["2012-11-26 11:24:00","2012-11-26 13:00:00")).

-- holiday
insert into schedule(during) values (tsrange('2012-11-28 10:00', '2012-11-28 13:00'));
ERROR:  public holiday
person mys    schedule 25.11.2012
comment
Спасибо большое. Превосходно. Кстати. Вы посмотрели обновленный вопрос в stackoverflow.com/questions/13433863/ . Похоже, что ваш ответ на этот вопрос возвращает неправильные данные, поскольку построенный выбор возвращает перекрывающиеся диапазоны. - person Andrus; 26.11.2012
comment
Почему вы создаете два триггера? Это можно сделать с помощью одного оператора create trigger holiday_check_i before insert or update ... ? Есть ли польза от двух? - person a_horse_with_no_name; 28.11.2012

Вы можете решить все, кроме пункта 3. с помощью простого CHECK< /strong> ограничения:

Используйте специальные функции диапазона. lower(anysrange) и upper(anyrange) для доступа к нижней/верхней границе диапазона.

1.) Даты начала и окончания в tsrange всегда одинаковы.

CONSTRAINT schedule_same_day
CHECK (lower(during)::date = upper(during)::date)

2.) Даты начала и окончания не могут быть субботой и воскресеньем.

Используйте isodow, а не dow для более простое выражение.

CONSTRAINT schedule_no weekend
CHECK (EXTRACT(ISODOW FROM lower(during)) < 6)  -- upper on same day

3.) Даты начала и окончания не могут отображаться в таблице государственных праздников.

Единственное исключение: для этого вам понадобится триггер, например:

CREATE OR REPLACE FUNCTION trg_during_no_holy()
  RETURNS trigger AS
$func$
BEGIN

IF EXISTS (SELECT 1 FROM holiday WHERE day = lower(NEW.during)) THEN
    RAISE EXCEPTION 'Day too holy: %', lower(NEW.during);
END IF;

RETURN NEW;

END
$func$ LANGUAGE plpgsql VOLATILE;

CREATE TRIGGER insupbef_holycheck
  BEFORE INSERT OR UPDATE
  ON schedule
  FOR EACH ROW
  EXECUTE PROCEDURE trg_during_no_holy();

4.) Время начала может быть только 8:00, 8:30, 9:00, 9:30, ... 16:00, 16:30, 17:00 или 17:30 включительно
5.) Окончание время может быть только 8:30, 9:00, 9:30, ... 16:00, 16:30, 17:00, 17:30 или 18:00 исключительно

CONSTRAINT schedule_8_inc_to_18_exc_half_hours
 CHECK (lower(during)::time BETWEEN time '8:00'AND time '17:30' -- time range
    AND upper(during)::time BETWEEN time '8:30'AND time '18:00'
    AND EXTRACT(MINUTE FROM lower(during)) IN (0, 30) -- only :00 or :30
    AND EXTRACT(MINUTE FROM upper(during)) IN (0, 30)
    AND lower_inc(during)          -- lower bound always incl.
    AND upper_inc(during) = FALSE  -- upper bound always excl.
)

Дополнительный вопрос в комментарии

Как ограничить секунды и доли секунды только до 0?

Самый простой способ: привести к timestamp(0) или timestamptz(0) или использовать этот тип данных для столбца с самого начала. Я цитирую руководство здесь:

time, timestamp и interval принимают необязательное значение точности p, которое указывает количество дробных цифр, сохраняемых в поле секунд.

person Erwin Brandstetter    schedule 25.11.2012
comment
Спасибо. Превосходно. Похоже, 4.) позволяет также не получасовое время, например 8:15, 8:0:01 и т. д. Как добавить ограничение, чтобы разрешено было только получасовое время? - person Andrus; 26.11.2012
comment
@Andrus: я добавил это, а также проверку инклюзивной/эксклюзивной границы. Вы можете разделить элементы на отдельные CHECK ограничения с очевидными именами для сообщений об ошибках... - person Erwin Brandstetter; 26.11.2012
comment
@ErwinBrandstetter Первое ограничение. Это не имеет смысла. Может быть, он хотел равные даты, а не дату/время. - person Ihor Romanchenko; 26.11.2012
comment
@Эрвин: спасибо. Это позволяет использовать любые значения секунд и долей секунды в отметке времени. Как ограничить секунды и доли секунды только до 0? - person Andrus; 27.11.2012
comment
@Andrus: я добавил главу к своему ответу. - person Erwin Brandstetter; 28.11.2012