I am really stuck on this because I dont understand how can I check for possible overlaps in such cases. So this is the condition of the task:
Imagine you are the manager of a hotel that has a table in its database with the following definition:
CREATE TABLE Hotel
(
Room SMALLINT NOT NULL,
Arrival DATE NOT NULL,
Departure DATE NOT NULL,
Guest CHAR(30),
PRIMARY KEY (room number, arrival)
CHECK (departure >= arrival)
);
So you can't leave this hotel before you've arrived. Now modify this definition so that no reservation can be entered in the table such as the Arrival/Departure date conflicts with an already existing reservation date. As overlaps count (examples):
already existing reservation 3.1.-6.1. with newly booked 1.1.‐5.1. or 4.1.-10.1.
or another existing 2.1.-6.1. with newly booked 1.1.-10.1. or 3.1.-5.1.
It also states, that it's OK to use selections from the Table inside of the CREATE statement of this same table, and use it in a CHECK constraint.
Before reading check the tutorial about triggers and how they work https://www.postgresqltutorial.com/postgresql-triggers/creating-first-trigger-postgresql/
CREATE TABLE if not exists Hotel
(
Room SMALLINT NOT NULL,
Arrival DATE NOT NULL,
Departure DATE NOT NULL,
Guest CHAR(30),
PRIMARY KEY (room, arrival),
CHECK (departure >= arrival)
);
CREATE OR REPLACE FUNCTION register_new_insert()
RETURNS TRIGGER
AS
$$
DECLARE
r hotel%rowtype;
BEGIN
FOR r IN
SELECT * FROM hotel WHERE Room = New.Room;
LOOP
IF r.arrival < new.arrival and new.arrival < r.departure
THEN RAISE EXCEPTION 'Arrival/Departure date conflicts with an already existing reservation date';
END IF;
IF r.arrival < new.departure and new.departure < r.departure
THEN RAISE EXCEPTION 'Arrival/Departure date conflicts with an already existing reservation date';
END IF;
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
CREATE TRIGGER register_new
BEFORE insert
ON Hotel
FOR EACH ROW
EXECUTE PROCEDURE register_new_insert();
You can create a daterange with existing arrival, depature columns and also with the new values, then apply the range overlaps operator (&&). (See demo);
create or replace function prevent_register_overlap()
returns trigger
language plpgsql
as $$
begin
if exists ( select null
from hotel h
where daterange(new. arrival, new.departure,'[]') &&
daterange(h.arrival, h.departure,'[]')
and new.room = h.room
)
then raise exception 'Arrival/Departure overlaps existing reservation.';
end if;
return new;
end;
$$;
create trigger register_new
before insert or update of arrival, departure
on hotel
for each row
execute procedure prevent_register_overlap();
As constructed these are closed ranges,both dates are included, see 8.17.6. Constructing Ranges for creating other types.
Related
Hi I'm trying to write a function in plpgsql that inserts values into a table as long as the dates given when running the function are not in between already existing dates for the same room number (it's a hotel data base so I'm trying to check if the room a person is trying to book isn't already occupied in the time the person wants to book said room). The problem is the function is ignoring the statment and inserting values in even when the room is already booked.
CREATE OR REPLACE FUNCTION new_booking(room_id_ INTEGER,check_in_date_ DATE,check_out_date_ DATE ,comment_ TEXT) RETURNS VOID AS $$
DECLARE
arrival DATE;
departure DATE;
BEGIN
SELECT check_in_date INTO arrival FROM booking WHERE room_id = room_id_;
SELECT check_out_date INTO departure FROM booking WHERE room_id = room_id_;
IF (check_in_date_ BETWEEN arrival AND departure OR check_out_date_ BETWEEN arrival AND departure) THEN
RAISE EXCEPTION 'This room is not available.';
ELSE
INSERT INTO booking(room_id ,booking_date,check_in_date,check_out_date,comment) VALUES (room_id_ ,CURRENT_DATE,check_in_date_,check_out_date_,comment_);
END IF;
END;
$$ LANGUAGE 'plpgsql';
I have the following tables:
CREATE TABLE review
(
review_id NUMBER(2) NOT NULL,
review_date DATE NOT NULL,
review_rating NUMBER(1) NOT NULL,
driver_no NUMBER(2) NOT NULL,
vehicle_id NUMBER(3) NOT NULL
);
CREATE TABLE testing
(
testing_id NUMBER(2) NOT NULL,
testing_start DATE NOT NULL,
testing_end DATE NOT NULL
driver_no NUMBER(2) NOT NULL,
vehicle_id NUMBER(3) NOT NULL
);
Basically vehicles are tested by drivers between two dates. After the testing is complete, the driver reviews the vehicle.
I want to create a trigger which will prevent an invalid review from being added. The review is invalid if the driver reviews the vehicle before the testing end date. The review is also invalid if the driver reviews a vehicle that he has not driven.
For example, driver 1 tests vehicle 7 from 01 Feb 2019 to 07 Feb 2019. If a review is added for 05 Feb 2019, I want the trigger to prevent this from being inserted. Also, if a review is added for vehicle 5 (when vehicle 7 was the one being tested), I want the trigger to prevent this from being inserted.
This is what I have so far:
CREATE OR REPLACE TRIGGER review_check_validity
AFTER INSERT ON review
FOR EACH ROW
BEGIN
SELECT testing_start
FROM testing
WHERE driver_no = :new.driver_no;
SELECT vehicle_id
FROM testing
WHERE driver_no = :new.driver_no;
IF :new.review_date < testing_end THEN
raise_application_error(-20000, 'Review date cannot be before
testing end date');
END IF;
IF :new.vehicle_id != vehicle_id THEN
raise_application_error(-20000, 'Driver has never driven this
vehicle');
END IF;
END;
/
The trigger compiles without any errors - however when I try to test it by inserting an invalid row into the REVIEW table, I get an error message stating
Exact fetch returns more than requested number of rows
Could somebody please point out what changes I need to make to my code to achieve the desired result?
I'd rearrange the logic here and:
Check if the driver has tested the vehicle, then
Check if the review is attempted before the testing end date for the vehicle (something you've left out).
In Oracle PL/SQL, which includes trigger code, you can't just SELECT. You have to SELECT INTO a variable. Then you can use the variable in your logic.
Equally important, when you SELECT INTO a variable the query can only return one result. Multiple rows will trigger the error you've encountered.
CREATE OR REPLACE TRIGGER review_check_validity
AFTER INSERT ON review
FOR EACH ROW
DECLARE
testEnd DATE;
vehicleTestCount NUMBER;
BEGIN
SELECT COUNT(*)
INTO vehicleTestCount
FROM testing
WHERE vehicle_id = :new.vehicle_id;
IF vehicleTestCount = 0 THEN
raise_application_error(-20000, 'Driver has never driven this vehicle');
END IF;
-- Assumes one test per driver per vehicle
SELECT testing_end
INTO testEnd
FROM testing
WHERE driver_no = :new.driver_no
AND vehicle_id = :new.vehicle_id;
IF :new.review_date < testEnd THEN
raise_application_error(-20000, 'Review date cannot be before
testing end date');
END IF;
END;
/
Finally, your table structure allows multiple tests of the same vehicle by the same driver. If it should allow this, then the review table should link to the testing table by testing_id rather than driver_no and vehicle_id.
I do think you should have an INTO clause in your SELECT fetches. I added some notes in your query to try to help you clearing it.
CREATE OR REPLACE TRIGGER review_check_validity
AFTER INSERT ON review
FOR EACH ROW
-- Need to declare variables for usage in your SELECT fetches.
DECLARE
var_revdate number; -- or date, if applicable
var_revvehi number; -- or varchar(n), if applicable
BEGIN
-- In here you should have an INTO clause to assign your date parameter into
-- the predefined variable. As well, you need to ensure this fetch will provide
-- only one row.
SELECT testing_start INTO var_revdate -- why are you fetching "testing_start"?
FROM testing
WHERE driver_no = :new.driver_no;
-- Same case, it can only retrieve one row. If you need to do more than one row,
-- you may need to use a BULK & a LOOP.
SELECT vehicle_id INTO var_vehicid
FROM testing
WHERE driver_no = :new.driver_no;
IF :new.review_date < testing_end THEN
raise_application_error(-20000, 'Review date cannot be before testing end date');
END IF;
IF :new.vehicle_id != var_vehicid THEN
raise_application_error(-20000, 'Driver has never driven this vehicle');
END IF;
END;
/
Hope that helps.
There are a few mistakes in your code:
In PL/SQL select queries must have an INTO clause where the data fetched from the SELECT query is stored into some variables. -- That part is missing in your code
You are searching in the testing table with the only driverno, which will give you all the records of that driverno(all the vehicles tested by that driveno). You need to include vehicleid along with driverno to fetch the details of testing relevant to the current review.
So Your trigger code can be re-written as follows:
CREATE OR REPLACE TRIGGER REVIEW_CHECK_VALIDITY
BEFORE INSERT ON REVIEW -- USING BEFORE INSERT TRIGGER TO AVOID ANY UNDOs
FOR EACH ROW
DECLARE
LV_TEST_END_DATE DATE;
LV_TEST_COUNT NUMBER;
BEGIN
-- FETCHING RELEVANT DATA USING SINGLE QUERY
SELECT
COUNT(1),
MAX(TESTING_END) -- PLEASE HANDLE THE SCENARIO WHERE THE TESTING END DATE IS NULL
INTO
LV_TEST_COUNT,
LV_TEST_END_DATE
FROM
TESTING
WHERE
VEHICLE_ID = :NEW.VEHICLE_ID
AND DRIVER_NO = :NEW.DRIVER_NO;
IF LV_TEST_COUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20000, 'Driver has never driven this vehicle');
ELSIF :NEW.REVIEW_DATE < LV_TEST_END_DATE THEN
RAISE_APPLICATION_ERROR(-20000, 'Review date cannot be before testing end date');
END IF;
END;
/
Cheers!!
Currently I'm creating an app that can essentially create post-it notes. I'm working on making my SQL tables for it. What I want to do is make it so the tables data is searchable by date. Multiple posts may be made on a day obviously. So I'm putting the date into a separate table. What I'm wondering is if it's possible to make it so the date column on the date table is not the current date that it will auto increment the Id and create a new column with the current date
CREATE TABLE IF NOT EXISTS ideas (
id SERIAL PRIMARY KEY,
ideas text,
date_id int );
CREATE TABLE IF NOT EXISTS date (
id SERIAL PRIMARY KEY,
table_date CONVERT(VARCHAR(15), GETDATE(),10));
Is the code I have so far any and all suggestions are welcome!
I would recommend using a TRIGGER procedure. You can trigger a function every time an insert is made on the ideas table. This function can check the dates table and make sure the current date exists in there. It can even set the new id of that date in the date_id column in the ideas table.
For example:
DROP TABLE IF EXISTS ideas;
CREATE TABLE ideas (
id SERIAL PRIMARY KEY,
ideas text,
date_id int
);
-- "date" is a reserved word. try to avoid naming a table "date".
DROP TABLE IF EXISTS dates;
CREATE TABLE dates (
id SERIAL PRIMARY KEY,
table_date DATE DEFAULT NOW() -- i would recommend the DATE type here
);
DROP TRIGGER IF EXISTS insert_date_if_absent ON ideas;
DROP FUNCTION IF EXISTS insert_date_if_absent();
CREATE FUNCTION insert_date_if_absent()
RETURNS TRIGGER
AS $$
DECLARE
today date := now();
new_date_id integer;
BEGIN
IF NOT EXISTS (SELECT * FROM dates WHERE table_date = today) THEN
INSERT INTO dates (table_date) VALUES (today) RETURNING id INTO new_date_id;
ELSE
SELECT id FROM dates WHERE table_date = today INTO new_date_id;
END IF;
IF NEW.date_id IS NULL THEN
NEW.date_id := new_date_id;
END IF;
RETURN NEW;
END
$$ LANGUAGE PLPGSQL;
CREATE TRIGGER insert_date_if_absent
BEFORE INSERT ON ideas
FOR EACH ROW
EXECUTE PROCEDURE insert_date_if_absent();
This will allow you to omit date_id when inserting into ideas. If omitted, it will get automatically set by the trigger to the id of today's date.
INSERT INTO ideas (ideas) VALUES ('sup dudeee');
Some other feedback which I incorporated in my answer:
Do not store dates as a VARCHAR, it's less efficient and more hassle. Use a DATE instead.
Do not name tables after reserved words in Postgres. Rather than date, name it dates.
I'm trying to follow the advice here to create a vertically partitioned table for storing time series data.
So far, my schema looks like this:
CREATE TABLE events
(
topic text,
t timestamp,
value integer,
primary key(topic, t)
);
CREATE TABLE events_2014
(
primary key (topic, t),
check (t between '2014-01-01' and '2015-01-01')
) INHERITS (events);
Now I'm trying to create an INSTEAD OF INSERT trigger so that events can be inserted on the events table and the row will end up in the right sub-table. But the documentation says that INSTEAD OF INSERT triggers can only be created on views, not tables (or subtables):
CREATE OR REPLACE FUNCTION insert_events () RETURNS TRIGGER AS $insert_events$ BEGIN
IF new.t between '2014-01-01' and '2015-01-01' THEN
INSERT INTO events_2014 SELECT new.*;
...
END IF
RETURN NULL;
END;
$insert_events$ LANGUAGE PLPGSQL;
CREATE TRIGGER insert_events INSTEAD OF INSERT ON events FOR EACH ROW EXECUTE PROCEDURE insert_events();
ERROR: "events" is a table
DETAIL: Tables cannot have INSTEAD OF triggers.
What's the right way of doing this?
You need to declare BEFORE INSERT triggers.
Documentation on partitioning is a great source of knowledge in this matter and is full of examples.
Example function from docs
CREATE OR REPLACE FUNCTION measurement_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
IF ( NEW.logdate >= DATE '2006-02-01' AND
NEW.logdate < DATE '2006-03-01' ) THEN
INSERT INTO measurement_y2006m02 VALUES (NEW.*);
ELSIF ( NEW.logdate >= DATE '2006-03-01' AND
NEW.logdate < DATE '2006-04-01' ) THEN
INSERT INTO measurement_y2006m03 VALUES (NEW.*);
...
ELSIF ( NEW.logdate >= DATE '2008-01-01' AND
NEW.logdate < DATE '2008-02-01' ) THEN
INSERT INTO measurement_y2008m01 VALUES (NEW.*);
ELSE
RAISE EXCEPTION 'Date out of range. Fix the measurement_insert_trigger() function!';
END IF;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
Example trigger from docs
CREATE TRIGGER insert_measurement_trigger
BEFORE INSERT ON measurement
FOR EACH ROW EXECUTE PROCEDURE measurement_insert_trigger();
Returning NULL from BEFORE trigger will keep the parent table empty.
Hi there I already seen other posts with the same error code, but I can't figure it out.
I have this table 'NOLEGGIO' created in this way:
CREATE TABLE NOLEGGIO(
idNoleggio INT PRIMARY KEY,
dataNoleggio DATE,
dataRestituzione DATE,
dataRestituito DATE,
CF CHAR(16) NOT NULL,
prezzo NUMBER(4),
--SEVERAL CONSTRAINTS...
All I want to do now is a trigger that sets a 'dataRestituzione' := :NEW.dataNoleggio + INTERVAL '3' DAY; (that means returnDate := :NEW.rentalDATE ) IF the date of membership is < than a specific date.
I show you my 'TESSERATO' table (tesserato stands for membership)
CREATE TABLE TESSERATO(
numTessera INT NOT NULL UNIQUE,
dataTesseramento DATE,
dataScadenza DATE,
CF CHAR(16) PRIMARY KEY,
-- CONSTRAINT...
If I execute the query outside my trigger (coming next) it works (because I have datas in the fields i'm looking at) but if I insert this query in the trigger, it doesn't work!
This is the trigger:
CREATE OR REPLACE TRIGGER TR_NOLEGGIO
BEFORE INSERT ON NOLEGGIO
FOR EACH ROW
DECLARE
DATAT DATE;
BEGIN
:NEW.idNoleggio := id_noleggio.NEXTVAL;
SELECT T.dataTesseramento INTO DATAT
FROM NOLEGGIO N JOIN TESSERATO T ON N.CF=T.CF
WHERE DATAT < TO_DATE('27/02/2014','DD/MM/YYYY');
/* Here I've even tried to do something like:
IF DATAT < TO_DATE.... THEN . But it doesn't work either.
However the query that actually works if I execute outside the trigger is the SELECT above.
*/
:NEW.dataRestituzione := :NEW.dataNoleggio + INTERVAL '3' DAY;
END;
/
It says No data Found error, while there are datas in the rows instead!! (In fact doing the select outside the trigger matches several rows).
It's definitely driving me crazy ! Cannot understand what I do wrong.
Thank you in advance for anyone that get involved into this.
Insert staments for the two tables
-- NOLEGGIO
INSERT INTO NOLEGGIO VALUES(001,'18-OTT-2013','20-OTT-2013',NULL,'P3SDTI85A15H501H',10);
INSERT INTO NOLEGGIO VALUES(002,'15-NOV-2013','19-NOV-2013',NULL,'CNTNDR89T42F839M',700);
--idRental,dateRental,dateReturn,dateReturned,SSN,price)
-- TESSERATO
INSERT INTO TESSERATO(dataTesseramento,dataScadenza,CF) VALUES('07-set-2013','07-set-2014','RDLVRT70M08F205K');
-- SEVERAL INSERTS MORE
-- N.B. the numTessera is made with a sequence in another trigger
New Answer Following Comments
I have put together a test script for this. The new code used for the trigger seems to work correctly updating the return date if a valid membership exists within the date requirements set. Feel free to just take the trigger code and discard the rest, I have just included this as it is what I have used to verify that the trigger performs an update when it should:
CAUTION: I am dropping tables in this test to make it rerunable, so i would only recommend using the full script in a test environment
/**************** R U N O N C E ********************/
--CREATE OR REPLACE SEQUENCE id_noleggio
-- MINVALUE 0
-- MAXVALUE 1000000000
-- START WITH 1
-- INCREMENT BY 1
-- CACHE 20;
/********************************************************/
/****************** R E R U N A B L E ****************/
drop table NOLEGGIO;
drop table TESSERATO;
CREATE TABLE NOLEGGIO(
idNoleggio INT PRIMARY KEY,
dataNoleggio DATE,
dataRestituzione DATE,
dataRestituito DATE,
CF CHAR(16) NOT NULL,
prezzo NUMBER(4));
CREATE TABLE TESSERATO(
numTessera INT NOT NULL UNIQUE,
dataTesseramento DATE,
dataScadenza DATE,
CF CHAR(16) PRIMARY KEY);
-- TESSERATO
INSERT INTO TESSERATO(numTessera, dataTesseramento, dataScadenza, CF) VALUES(1, '15-NOV-2013','15-NOV-2014','ABCDEFGHI0000001');
INSERT INTO TESSERATO(numTessera, dataTesseramento, dataScadenza, CF) VALUES(2, '01-MAR-2014','01-MAR-2015','ABCDEFGHI0000002');
-- SEVERAL INSERTS MORE
-- N.B. the numTessera is made with a sequence in another trigger
CREATE OR REPLACE TRIGGER TR_NOLEGGIO
BEFORE INSERT ON NOLEGGIO
FOR EACH ROW
DECLARE
CUT_OFF_DATE DATE := TO_DATE('27/02/2014','DD/MM/YYYY');
MEMBER_EXISTS VARCHAR2(1) := 'N';
DATAT DATE;
BEGIN
:NEW.idNoleggio := id_noleggio.NEXTVAL;
-- membership exists
SELECT 'Y', T.dataTesseramento
INTO MEMBER_EXISTS, DATAT
FROM TESSERATO T
WHERE T.CF = :NEW.CF
AND T.dataTesseramento < CUT_OFF_DATE;
-- if value returned from query above is not null...
if MEMBER_EXISTS = 'Y' then
:NEW.dataRestituzione := :NEW.dataNoleggio + INTERVAL '3' DAY;
end if;
exception
when no_data_found then
-- e.g. if there are no records in the TESSERATO table with the same CF value
null; -- no action required, this will just stop an error being flagged
END;
/
-- test trigger
-- should set dataRestituzione (a valid membership exists within date requirements)
INSERT INTO NOLEGGIO VALUES(004, '01-Mar-2014', NULL, NULL, 'ABCDEFGHI0000001', 20); -- should set dataRestituzione
-- should not set dataRestituzione (membership too recent)
INSERT INTO NOLEGGIO VALUES(004, '01-Mar-2014', NULL, NULL, 'ABCDEFGHI0000002', 30);
-- should not set dataRestituzione (no record of membership in TESSERATO table)
INSERT INTO NOLEGGIO VALUES(1, '18-OCT-2013', NULL, NULL, 'P3SDTI85A15H501H', 10);
INSERT INTO NOLEGGIO VALUES(2, '15-NOV-2013', NULL, NULL, 'CNTNDR89T42F839M', 700);
--idRental,dateRental,dateReturn,dateReturned,SSN,price)
-- look at results
select * from TESSERATO;
select * from NOLEGGIO;
I think that the key problem with the way that you were trying to do this before is that you were joining to the NOLEGGIO table to retrieve data that had not yet been inserted.
Previous Answer
Try chaining the line:
WHERE DATAT < TO_DATE('27/02/2014','DD/MM/YYYY');
to:
WHERE T.dataTesseramento < TO_DATE('27/02/2014','DD/MM/YYYY');
It looks like you are using this variable for a where condition before you have assigned a value to it i.e. it doesn't know the value if DATAT until the query has completed, but you are trying to use this value within the query.