Postgres date overlapping constraint - sql

I have a table like this:
date_start date_end account_id product_id
2001-01-01 2001-01-31 1 1
2001-02-01 2001-02-20 1 1
2001-04-01 2001-05-20 1 1
I want to disallow overlapping intervals a given (account_id, product_id)
EDIT: I found something:
CREATE TABLE test (
from_ts TIMESTAMPTZ,
to_ts TIMESTAMPTZ,
account_id INTEGER,
product_id INTEGER,
CHECK ( from_ts < to_ts ),
CONSTRAINT overlapping_times EXCLUDE USING GIST (
account_id WITH =,
product_id WITH =,
box(
point( extract(epoch FROM from_ts at time zone 'UTC'), extract(epoch FROM from_ts at time zone 'UTC') ),
point( extract(epoch FROM to_ts at time zone 'UTC') , extract(epoch FROM to_ts at time zone 'UTC') )
) WITH &&
)
);
If you want to know more about this http://www.depesz.com/2010/01/03/waiting-for-8-5-exclusion-constraints/
My only problem is that it doesn't work with null values as a ending timestamp, I thought of replace it with infinite values but does not work as well.

Ok i ended up doing this :
CREATE TABLE test (
from_ts TIMESTAMPTZ,
to_ts TIMESTAMPTZ,
account_id INTEGER DEFAULT 1,
product_id INTEGER DEFAULT 1,
CHECK ( from_ts < to_ts ),
CONSTRAINT overlapping_times EXCLUDE USING GIST (
account_id WITH =,
product_id WITH =,
period(from_ts, CASE WHEN to_ts IS NULL THEN 'infinity' ELSE to_ts END) WITH &&
)
);
Works perfectly with infinity, transaction proof.
I just had to install temporal extension which is going to be native in postgres 9.2 and btree_gist available as an extension in 9.1 CREATE EXTENSION btree_gist;
nb : if you don't have null timestamp there is no need to use the temporal extension you could go with the box method as specified in my question.

In up to date postgres versions (I tested it in 9.6 but I assume it's working in >=9.2) you can use the build in function tstzrange() as mentioned in some other comments. Null values will be treated as positive or negative infinity by default and the CHECK contraint is then not explicitly needed anymore (if you are fine that the check is only <= and a range can start and end with the same date). Only the extension btree_gist is still needed:
CREATE EXTENSION btree_gist;
CREATE TABLE test (
from_ts TIMESTAMPTZ,
to_ts TIMESTAMPTZ,
account_id INTEGER DEFAULT 1,
product_id INTEGER DEFAULT 1,
CONSTRAINT overlapping_times EXCLUDE USING GIST (
account_id WITH =,
product_id WITH =,
TSTZRANGE(from_ts, to_ts) WITH &&
)
);

This is a difficult problem because constraints can only reference the "current row", and may not contain subqueries. (otherwise the trivial solution would be to add some NOT EXISTS() subquery in the check)
A check constraint specified as a column constraint should reference that column's value only, while an expression appearing in a table constraint can reference multiple columns.
Currently, CHECK expressions cannot contain subqueries nor refer to variables other than columns of the current row.
Popular work-arounds are: use a trigger function which does the dirty work (or use the rule system, which is deprecated by most people)
Because most people favor triggers, I'll repost a rule-system hack here... (it does not have the extra "id" key element, but that's a minor detail)
-- Implementation of A CONSTRAINT on non-overlapping datetime ranges
-- , using the Postgres rulesystem.
-- We need a shadow-table for the ranges only to avoid recursion in the rulesystem.
-- This shadow table has a canary variable with a CONSTRAINT (value=0) on it
-- , and on changes to the basetable (that overlap with an existing interval)
-- an attempt is made to modify this variable. (which of course fails)
-- CREATE SCHEMA tmp;
DROP table tmp.dates_shadow CASCADE;
CREATE table tmp.dates_shadow
( time_begin timestamp with time zone
, time_end timestamp with time zone
, overlap_canary INTEGER NOT NULL DEFAULT '0' CHECK (overlap_canary=0)
)
;
ALTER table tmp.dates_shadow
ADD PRIMARY KEY (time_begin,time_end)
;
DROP table tmp.dates CASCADE;
CREATE table tmp.dates
( time_begin timestamp with time zone
, time_end timestamp with time zone
, payload varchar
)
;
ALTER table tmp.dates
ADD PRIMARY KEY (time_begin,time_end)
;
CREATE RULE dates_i AS
ON INSERT TO tmp.dates
DO ALSO (
-- verify shadow
UPDATE tmp.dates_shadow ds
SET overlap_canary= 1
WHERE (ds.time_begin, ds.time_end) OVERLAPS ( NEW.time_begin, NEW.time_end)
;
-- insert shadow
INSERT INTO tmp.dates_shadow (time_begin,time_end)
VALUES (NEW.time_begin, NEW.time_end)
;
);
CREATE RULE dates_d AS
ON DELETE TO tmp.dates
DO ALSO (
DELETE FROM tmp.dates_shadow ds
WHERE ds.time_begin = OLD.time_begin
AND ds.time_end = OLD.time_end
;
);
CREATE RULE dates_u AS
ON UPDATE TO tmp.dates
WHERE NEW.time_begin <> OLD.time_begin
AND NEW.time_end <> OLD.time_end
DO ALSO (
-- delete shadow
DELETE FROM tmp.dates_shadow ds
WHERE ds.time_begin = OLD.time_begin
AND ds.time_end = OLD.time_end
;
-- verify shadow
UPDATE tmp.dates_shadow ds
SET overlap_canary= 1
WHERE (ds.time_begin, ds.time_end) OVERLAPS ( NEW.time_begin, NEW.time_end)
;
-- insert shadow
INSERT INTO tmp.dates_shadow (time_begin,time_end)
VALUES (NEW.time_begin, NEW.time_end)
;
);
INSERT INTO tmp.dates(time_begin,time_end) VALUES
('2011-09-01', '2011-09-10')
, ('2011-09-10', '2011-09-20')
, ('2011-09-20', '2011-09-30')
;
SELECT * FROM tmp.dates;
EXPLAIN ANALYZE
INSERT INTO tmp.dates(time_begin,time_end) VALUES ('2011-09-30', '2011-10-04')
;
INSERT INTO tmp.dates(time_begin,time_end) VALUES ('2011-09-02', '2011-09-04')
;
SELECT * FROM tmp.dates;
SELECT * FROM tmp.dates_shadow;

How to create unique constraint to a group of columns :
CREATE TABLE table (
date_start date,
date_end date,
account_id integer,
UNIQUE (account_id , date_start ,date_end) );
in your case you will need to ALTER TABLE if the table already exists, check the documentation it would be helpful for you :
- DDL Constraints
- ALTER Table

Related

CUSTOM UNIQUE CHECK POSTGRES

I am working on a task where I need to store the interviewer's time slot in the table INTERVIEW_SLOT. The table schema is like this:
CREATE TABLE INTERVIEW_SLOT (
ID SERIAL PRIMARY KEY NOT NULL,
INTERVIEWER INTEGER REFERENCES USERS(ID) NOT NULL,
START_TIME TIMESTAMP NOT NULL, -- start time of interview
END_TIME TIMESTAMP NOT NULL, -- end time of interview
IS_BOOKED BOOL NOT NULL DEFAULT 'F', -- slot is booked by any candidate or not
CREATED_ON TIMESTAMP,
-- interviewer can't give the same slot twice
CONSTRAINT UNIQUE_INTERVIEW_SLOT UNIQUE (start_time, INTERVIEWER)
);
We want to ensure that the interviewer can not give the same slot twice but the problem is with second and millisecond values of start_time. I want the UNIQUE_INTERVIEW_SLOT constant like this:
UNIQUE_INTERVIEW_SLOT UNIQUE(TO_TIMESTAMP(start_time::text, 'YYYY-MM-DD HH24:MI'), INTERVIEWER)
Is there any way to add a unique constraint that ignores the second and millisecond value?
You are looking for an exclusion constraint
create table interview_slot
(
id integer primary key generated always as identity,
interviewer integer references users(id) not null,
start_time timestamp not null, -- start time of interview
end_time timestamp not null, -- end time of interview
is_booked bool not null default 'f', -- slot is booked by any candidate or not
created_on timestamp,
constraint unique_interview_slot
exclude using gist (interviewer with =,
tsrange(date_trunc('minute', start_time), date_trunc('minute', end_time), '[]') with &&)
);
This prevents rows with overlapping start/end ranges for the same interviewer. The timestamps are "rounded" to the full minute. You need the extension btree_gist in order to create that constraint.
You can use an UNIQUE INDEX to make this check for you and truncate the timestamp to minutes:
CREATE UNIQUE INDEX idx_interview_slot_ts
ON interview_slot (interviewer, date_trunc('minutes',start_time));
Demo: db<>fiddle

constraint on overlapping intervals in postgres records

So I am making a booking system and I have the following table for reservations.
CREATE TABLE reservations (
user_id INT REFERENCES users (id),
booth_number INT REFERENCES booths (booth_number),
starts DATE NOT NULL,
ends DATE NOT NULL,
PRIMARY KEY (user_id, booth_number)
);
and I would like to know if it is possible to put a constraint on a database level that disallows insertion on overlapping reservations on the same booth?
e.g
INSERT INTO reservations( user_id, booth_number, starts, ends)
VALUES
(1, 0, CURRENT_DATE, CURRENT_DATE + 7),
(2, 0, CURRENT_DATE + 4, CURRENT_DATE + 9),
Almost the exact scenario(room instead of booth) is in the docs:
https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-CONSTRAINT
"
You can use the btree_gist extension to define exclusion constraints on plain scalar data types, which can then be combined with range exclusions for maximum flexibility. For example, after btree_gist is installed, the following constraint will reject overlapping ranges only if the meeting room numbers are equal:"
CREATE EXTENSION btree_gist;
CREATE TABLE room_reservation (
room text,
during tsrange,
EXCLUDE USING GIST (room WITH =, during WITH &&)
);
To modify for your use:
EXCLUDE USING GIST (booth_number WITH =, daterange(starts, ends, '[]') WITH &&))

PostgreSQL Why sequence increases only once with two "nextval"

I am implementing a board with a comment function.
I created a table like this with nextval
create table tr_board
(
b_id varchar(20) not null default nextval('seq_board'),
title varchar(256),
content varchar(500),
user_id varchar(30),
update_date timestamp not null,
is_complete varchar(1) default 0,
is_private varchar(1) default 0
);
and this is my Insert sql.
<insert id="addBoard" parameterType="java.util.HashMap">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select nextval('seq_board') as id;
</selectKey>
insert into tr_board
(
b_id,
title,
content,
user_id,
is_private,
update_date
)
values
(
#{id},
#{title},
#{content},
#{user_id},
#{is_private},
current_timestamp at time zone 'utc'
)
</insert>
I used nextval in the "default" of "create" and "insert".
So I thought the sequence would increase twice.
But Why is the sequence increasing only once, even though a total of two nextval exists?
I would appreciate your reply.
The nextval() on the column default only gets called if a value isn't provided, or you explicitly ask for the default to be used. You are getting a sequence value, and then passing that as a value to the b_id column, so it doesn't try to get the next value in the sequence because no default is needed. It would only call it twice if you got the value, and then did nothing with it, requiring the default to be used.

Postgres - range for 'time without time zone' and exclude constraint

I have the following table:
create table booking (
identifier integer not null primary key,
room uuid not null,
start_time time without time zone not null,
end_time time without time zone not null
);
I want to create an exclude constraint to enforce that there are no overlapping appointments for the same room.
I tried the following:
alter table booking add constraint overlapping_times
exclude using gist
(
cast(room as text) with =,
period(start_time, end_time) with &&)
);
This has two problems:
Casting room to text is not enough, it gives: ERROR: data type text has no default operator class for access method "gist". I know in v10 there is btree_gist, but I am using v9.5 and v9.6, so I have to manually cast the uuid to a text afaik.
period(...) is wrong, but I have no idea how to construct a range of time without time zone type.
After installing btree_gist, you can do the following:
create type timerange as range (subtype = time);
alter table booking add constraint overlapping_times
exclude using gist
(
(room::text) with =,
timerange(start_time, end_time) with &&
);
If you want an expression in the constraint you need to put that into parentheses. So either (room::text) or (cast(room as text))

Redshift: creating a table with Timestamp column defaulting to Now()

Is there a way to create a table with a timestamp column defaulting to now?
Any of the following succeeds on creation but fails on insertion.
CREATE TABLE MyTable
(
device_id VARCHAR(64) NOT NULL encode lzo,
app_id VARCHAR(150) NOT NULL distkey encode lzo,
retrieval_date TIMESTAMP without time zone default (now() at time zone 'utc') encode lzo
)
diststyle key
compound sortkey(app_id,device_id);
Then on insert:
insert into MyTable (device_id,app_id) values('a','b');
INFO: Function "timezone(text,timestamp with time zone)" not supported.
INFO: Function "now()" not supported.
ERROR: Specified types or functions (one per INFO message) not supported on Redshift tables.
I tried other flavors as below but all fail.
a) Tried with now in single quotes 'now' , create succeeded but failed with another error
b) Tried without the timezone, create succeeded, insert failed.
You can use SYSDATE or GETDATE() to put a current timestamp value. Here is an example.
dev=> create table my_table (id int, created_at datetime default sysdate);
CREATE TABLE
dev=> insert into my_table (id) values (1);
INSERT 0 1
dev=> select * from my_table;
id | created_at
----+---------------------------
1 | 2016-01-04 19:07:14.18745
(1 row)
SYSDATE (Oracle Compatibility Function)
http://docs.aws.amazon.com/redshift/latest/dg/r_SYSDATE.html
GETDATE()
http://docs.aws.amazon.com/redshift/latest/dg/r_GETDATE.html