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';
Related
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.
CREATE OR REPLACE FUNCTION inserts_table_foo(startId INT,
endId INT,
stepTimeZone INT default 0,
period INT default 0) RETURNS void AS
$$
DECLARE
nameVar TEXT;
dateVar TIMESTAMP;
begin
for idVar in startId..endId
loop
nameVar := md5(random()::text);
dateVar :=
(now()::timestamp with time zone +
make_interval(hours := stepTimeZone) +
make_interval(days := period)
);
period := period + 1;
insert into foo(id, name, data) values (idVar, nameVar, dateVar);
end loop;
end ;
$$ LANGUAGE plpgsql;
Unfortunately, the variable -> period, does not work as intended. I assumed that the counter would increase by 1 and each time, the date generation would be different, but in fact, the date turns out to be the same for the entire duration of the cycle.
How to make the date, for each iteration, be generated anew? This won't do here
generate_series ( TIMESTAMP WITHOUT TIME ZONE '2022-01-01', CURRENT_DATE, '1 day' )
because, you need to bet 100,000,000 records, in a table where the date field can be in the table in several places (dateStart, dateEnd, startDateReg, etc.) and are placed out of order. Therefore, I want to make a function that accepts a number of variables, according to which I can set the periods of the generated values, in this example, this is the date and time.
I would like to clearly see which variable the value comes into and also see the code for inserting this value into the table (that is, using the INSERT into entry .....)
What is the problem here and how could it be solved ?
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!!
I'm trying to get the average of the field pm10_ug_m3 of all the values introduced in the last 24 hours by a sensor, but with my current code the average does not include the value of the row inserted.
Currently as the trigger is done before the insert, it is not taking into account the last value inserted in the field pm10_ug_m3. For this reason the average introduced in the field is 3 and not 6.5 obtained from (10+3)/2
1) Creation of table and addition of some dates:
(
ID bigint NOT NULL,
SensorID character(10),
pm10_ug_m3 numeric(10,2),
tense timestamp without time zone,
average float,
CONSTRAINT id_pk PRIMARY KEY (ID)
);
INSERT INTO sensor (ID,SensorID,pm10_ug_m3,tense) VALUES
(1,'S16',1,'2019-07-10 04:25:59'),
(2,'S20',3,'2017-07-10 02:25:59');
2) Creation of the trigger to calculate the average of pm10_ug_m3 of the records captured in the last 24h from the same sensor:
CREATE OR REPLACE FUNCTION calculate_avg() RETURNS TRIGGER AS $BODY$
BEGIN
NEW.average := ( SELECT AVG(pm10_ug_m3)
FROM sensor
WHERE SensorID=NEW.SensorID
AND tense>= NEW.tense - INTERVAL '24 HOURS');
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER calculate_avg_trigger BEFORE INSERT OR UPDATE
ON sensor FOR EACH ROW
EXECUTE PROCEDURE calculate_avg();
3) Insert of a new row, where it will be populated the field average:
INSERT INTO sensor (ID,SensorID,pm10_ug_m3,tense) VALUES
(3,'S20',10,'2017-07-10 04:25:59')
This does not work because the AVG() function only considers the data which is still inserted, not the new data which will be inserted.
Changing the trigger point from BEFORE to AFTER would deliver a correct result indeed, but it will not be set because the INSERT already has been done at this point.
So one way to achieve your result is to calculate the average manually in your trigger function:
SELECT (SUM(pm10_ug_M3) + NEW.pm10_ug_m3) / (COUNT(pm10_ug_m3) + 1)
FROM ...
SUM() of the current values + the new divided by the COUNT() of the current values + the new one.
demo:db<>fiddle
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.