I'm pretty new to Postgres and SQL as a whole and could use a bit of help with a function here.
I have this table:
CREATE TABLE car_rentals(
plate varchar(10) NOT NULL,
start_date date NOT NULL,
end_date date NOT NULL,
license_nr varchar(10) NOT NULL,
CONSTRAINT unq_car_rentals_start UNIQUE (plate, start_date),
CONSTRAINT unq_car_rentals_end UNIQUE (plate, end_date));
What i need is for a function to take as input plate, start date and end date and
throw an error if the table contains any row with the same plate where the rental period is different
from but overlaps with the given period.
I found this document on reporting errors but I'm not sure how to properly format the function in order to implement what I need.
https://www.postgresql.org/docs/9.6/plpgsql-errors-and-messages.html
You don't need a trigger to achieve this. Postgres supports so called exclusion constraints exactly for this purpose:
create table car_rentals
(
plate varchar(10) NOT NULL,
start_date date NOT NULL,
end_date date NOT NULL,
license_nr varchar(10) NOT NULL,
constraint no_overlapping_ranges
exclude using gist (plate with =, daterange(start_date, end_date, '[]') with &&)
);
The constraint definition in details:
plate with = for rows with the same plate number
daterange(start_date, end_date, '[]') with && - fail if there are rows with an overlapping range.
As the constraint includes the = operator, you need to install the extension btree_gist in order to create the above constraint:
As a superuser run:
create extension btree_gist;
in the database where you want to create the car_rentals database (you only need to do this once).
Online example
The WHERE condition of a query you could use in a trigger could be:
WHERE daterange(start_date, end_date, '[]') && daterange($1, $2, '[]') -- overlaps
AND (start_date <> $1 OR end_date <> $2) -- is not equal
AND plate = $3
Here $1, $2 and $3 are placeholders for the data against which you want to check, and the query becomes simple by using the "overlaps" range operator &&.
Related
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 &&))
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))
This question already has answers here:
CHECK constraint on date of birth?
(5 answers)
Closed 8 years ago.
CREATE TABLE "TEST"."AB_EMPLOYEE22"
( "NAME" VARCHAR2(20 BYTE),
"AGE" NUMBER,
"SALARY" NUMBER,
"DOB" DATE
)
alter table "TEST"."AB_EMPLOYEE22" add constraint
Age_check check((ROUND((sysdate-DOB)/365)) = AGE) ENABLE
But This query is not working.
Pls help me out
Disclaimer: It's not a direct answer to the question.
Now, don't store derived and most importantly constantly changing data such as age in the table. Instead calculate it on the fly when you query it (e.g. with a view).
CREATE TABLE ab_employee22
(
name VARCHAR2(20),
salary NUMBER,
dob DATE
);
CREATE VIEW ab_employee22_view AS
SELECT name, salary, dob,
FLOOR(MONTHS_BETWEEN(sysdate, dob) / 12) age
FROM ab_employee22;
Here is SQLFIddle demo
You cannot use SYSDATE in check constraint. According to Oracle Documentation - Check Constraint
Conditions of check constraints cannot
contain the following constructs:
Subqueries and scalar subquery expressions
Calls to the functions that are not deterministic (CURRENT_DATE,
CURRENT_TIMESTAMP, DBTIMEZONE,
LOCALTIMESTAMP, SESSIONTIMEZONE,
SYSDATE, SYSTIMESTAMP, UID, USER, and
USERENV)
Calls to user-defined functions
Dereferencing of REF columns (for example, using the DEREF function)
Nested table columns or attributes
The pseudocolumns CURRVAL, NEXTVAL, LEVEL, or ROWNUM
Date constants that are not fully specified
So, you can use Trigger to get your desired output in this case. Here, is the trigger which will work fine as per your requirement:
CREATE OR REPLACE TRIGGER trg_check_date
BEFORE INSERT OR UPDATE ON AB_EMPLOYEE22
FOR EACH ROW
BEGIN
IF(ROUND((sysdate-nvl(:NEW.DOB,:OLD.DOB))/365) <> nvl(:NEW.AGE,:OLD.AGE))
THEN
RAISE_APPLICATION_ERROR( -20001, 'Your Date of Birth and Age do not match');
END IF;
END;
If you find any difficulty in this trigger, please feel free to write in comments.
It is not possible to user system variables like 'Sysdate' in a constraint.
Link: http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:465820332843
suggestion is to use a trigger with the same logic
Forget about having SYSDATE since its not valid or using a trigger for a workaround. I will show you a cheap trick!!!
Write sysdate into a column and use it for validation. This column might be your audit column (For eg: creation date)
CREATE TABLE "AB_EMPLOYEE22"
(
"NAME" VARCHAR2 ( 20 BYTE ),
"AGE" NUMBER,
"SALARY" NUMBER,
"DOB" DATE,
"DOJ" DATE DEFAULT SYSDATE
);
Table Created
ALTER TABLE "AB_EMPLOYEE22" ADD CONSTRAINT
AGE_CHECK CHECK((ROUND((DOJ-DOB)/365)) = AGE) ENABLE;
Table Altered
You have missed an parameter of round function.
alter table AB_EMPLOYEE22 add constraint
Age_check check((ROUND((sysdate-DOB)/365,0)) = AGE) ENABLE
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
Let's say I have one table called ProjectTimeSpan (which I haven't, just as an example!) containing the columns StartDate and EndDate.
And that I have another table called SubProjectTimeSpan, also containing columns called StartDate and EndDate, where I would like to set a Check constraint that makes it impossible to set StartDate and EndDate to values "outside" the ProjectTimeSpan.StartDate to ProjectTimeSpan.EndDate
Kind of a Check constraint that knows about another tables values...
Is this possible?
In response to your comment on GSerg's answer, here's an example check constraint using a function:
alter table YourTable
add constraint chk_CheckFunction
check (dbo.CheckFunction() = 1)
Where you can define the function like:
create function dbo.CheckFunction()
returns int
as begin
return (select 1)
end
The function is allowed to reference other tables.
You can create a user-defined function that does the check and returns 1 or 0, then create a check constraint on it, providing project id and the dates as the parameters.
Make a compound key of the ProjectTimeSpan table's key combined with the StartDate and EndDate columns, then use this compound key for your foreign key reference in your SubProjectTimeSpan table. This will give you the ability to write the necessary row-level CHECK constraints in the SubProjectTimeSpan table e.g.
CREATE TABLE ProjectTimeSpan
(
project_ID INTEGER NOT NULL UNIQUE, -- key
StartDate DATE NOT NULL,
EndDate DATE NOT NULL,
CHECK (StartDate < EndDate),
UNIQUE (project_ID, StartDate, EndDate) -- compound key
-- other project columns here...
);
CREATE TABLE SubProjectTimeSpan
(
project_ID INTEGER NOT NULL,
StartDate DATE NOT NULL,
EndDate DATE NOT NULL,
FOREIGN KEY (project_ID, StartDate, EndDate)
REFERENCES ProjectTimeSpan (project_ID, StartDate, EndDate)
ON DELETE CASCADE
ON UPDATE CASCADE,
sub_StartDate DATE NOT NULL,
sub_EndDate DATE NOT NULL,
CHECK (sub_StartDate < sub_EndDate),
CHECK (StartDate <= sub_StartDate), -- sub project can't start before main project
CHECK (sub_EndDate <= EndDate) -- sub project can't end after main project
-- other sub project columns here...
);
You certainly can do this as many answers have shown. However, you should be aware that SQL Server seems to have trouble with CHECK CONSTRAINTs that use UDFs:
https://dba.stackexchange.com/questions/12779/how-are-my-sql-server-constraints-being-bypassed
You need to add constraint on the parent and the children table because the subproject can't be out of the project range but the project range can't move out of all the subproject too.
In these kind of situations, you should defer the check of the constraint on an upper level (webservice, application) with a transaction to ensure your data are in a valid state after multiple query on both table !
It is absolutely possible, and actually quite simple. As per your example:
create or alter function dbo.Check_SubProjectTimeSpan_ProjectTimeSpan_Dates(
#ProjectTimeSpanId int
, #StartDate date
, #EndDate date
)
returns bit
as
begin
if exists (select *
from dbo.ProjectTimeSpan as pts
where pts.Id = #ProjectTimeSpanId
and pts.StartDate >= #StartDate
and pts.EndDate <= #EndDate)
begin
return 1
end
return 0
end
go
alter table dbo.SubProjectTimeSpan add constraint
CK_SubProjectTimeSpan_ProjectTimeSpan_ProjectTimeSpanId_StartDate_EndDate
check (
dbo.Check_SubProjectTimeSpan_ProjectTimeSpan_Dates(
ProjectTimeSpanId, StartDate, EndDate) = 1
)
Please be aware however, that this will not be particularly performant.