Changes between Initial Version and Version 1 of AdvancedDatabaseDevelopment


Ignore:
Timestamp:
07/03/26 13:33:54 (8 days ago)
Author:
223091
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedDatabaseDevelopment

    v1 v1  
     1= Advanced Database Development =
     2
     3This page documents the advanced database development implemented for Phase P7 of the Room Reservation System project. The goal of this phase is to implement non-trivial database-level business rules and supporting database objects using triggers, stored functions, views, and a custom domain.
     4
     5The implementation extends the database created in the previous phases. It does not replace the relational schema from Phase P2, but it adds advanced database programming objects that enforce important consistency requirements and support the application logic more directly inside PostgreSQL.
     6
     7The SQL script for this phase is attached to this page:
     8
     9[attachment:advanced_database_development.sql advanced_database_development.sql]
     10
     11== Data constraints requirements: Prevent overlapping active room reservations ==
     12
     13=== Data requirements description ===
     14
     15A room must not have two active reservations that overlap in time. A reservation is considered active when its status is either ''pending'' or ''approved''. Rejected and cancelled reservations do not block the room.
     16
     17The basic unique constraint from Phase P2 only prevents identical reservation intervals for the same room. However, it does not prevent partially overlapping intervals. For example, if a room is reserved from 09:00 to 10:00, another reservation from 09:30 to 10:30 should not be allowed.
     18
     19This requirement is non-trivial because it depends on comparing the new or updated reservation with other reservations for the same room, same date, active status, and overlapping time intervals.
     20
     21=== Implementation ===
     22
     23The following trigger function checks whether a new or updated reservation overlaps with an existing active room reservation.
     24
     25{{{
     26CREATE OR REPLACE FUNCTION project.fn_prevent_room_overlap()
     27RETURNS TRIGGER AS $$
     28BEGIN
     29IF NEW.room_id IS NULL OR NEW.status NOT IN ('pending', 'approved') THEN
     30RETURN NEW;
     31END IF;
     32
     33```
     34IF EXISTS (
     35    SELECT 1
     36    FROM project.reservations r
     37    WHERE r.room_id = NEW.room_id
     38      AND r.reservation_date = NEW.reservation_date
     39      AND r.status IN ('pending', 'approved')
     40      AND r.reservation_id <> COALESCE(NEW.reservation_id, -1)
     41      AND NEW.start_time < r.end_time
     42      AND NEW.end_time > r.start_time
     43) THEN
     44    RAISE EXCEPTION
     45        'Room reservation overlap detected for room_id %, date %, time interval % - %.',
     46        NEW.room_id,
     47        NEW.reservation_date,
     48        NEW.start_time,
     49        NEW.end_time;
     50END IF;
     51
     52RETURN NEW;
     53```
     54
     55END;
     56$$ LANGUAGE plpgsql;
     57}}}
     58
     59The trigger is executed before inserting or updating a reservation.
     60
     61{{{
     62CREATE TRIGGER trg_prevent_room_overlap
     63BEFORE INSERT OR UPDATE OF room_id, reservation_date, start_time, end_time, status
     64ON project.reservations
     65FOR EACH ROW
     66EXECUTE FUNCTION project.fn_prevent_room_overlap();
     67}}}
     68
     69=== Test example ===
     70
     71{{{
     72INSERT INTO project.reservations (
     73room_id,
     74user_id,
     75reservation_date,
     76start_time,
     77end_time,
     78status
     79)
     80VALUES (
     813,
     821,
     83DATE '2026-03-01',
     84TIME '09:30',
     85TIME '10:30',
     86'pending'
     87);
     88}}}
     89
     90If there is already an active reservation for the same room and overlapping time interval, the trigger rejects the insert.
     91
     92== Data constraints requirements: Reservation must contain at least one resource ==
     93
     94=== Data requirements description ===
     95
     96A reservation may be created for:
     97
     98* only a room;
     99* only equipment;
     100* both a room and equipment.
     101
     102However, a reservation without both a room and requested equipment is invalid. This requirement cannot be enforced by a simple column-level CHECK constraint because the requested equipment is stored in a separate relation, ''project.reservation_equipment''.
     103
     104Therefore, a deferred constraint trigger is used. The trigger checks the rule at transaction commit time, after all related rows have been inserted.
     105
     106=== Implementation ===
     107
     108The trigger function checks whether the reservation has either a room or at least one requested equipment item.
     109
     110{{{
     111CREATE OR REPLACE FUNCTION project.fn_check_reservation_has_resource()
     112RETURNS TRIGGER AS $$
     113DECLARE
     114v_has_equipment BOOLEAN;
     115BEGIN
     116SELECT EXISTS (
     117SELECT 1
     118FROM project.reservation_equipment re
     119WHERE re.reservation_id = NEW.reservation_id
     120)
     121INTO v_has_equipment;
     122
     123```
     124IF NEW.room_id IS NULL AND NOT v_has_equipment THEN
     125    RAISE EXCEPTION
     126        'Reservation % must include at least one resource: either a room or requested equipment.',
     127        NEW.reservation_id;
     128END IF;
     129
     130RETURN NEW;
     131```
     132
     133END;
     134$$ LANGUAGE plpgsql;
     135}}}
     136
     137The trigger is created as a deferred constraint trigger.
     138
     139{{{
     140CREATE CONSTRAINT TRIGGER ctg_reservation_has_resource
     141AFTER INSERT OR UPDATE OF room_id
     142ON project.reservations
     143DEFERRABLE INITIALLY DEFERRED
     144FOR EACH ROW
     145EXECUTE FUNCTION project.fn_check_reservation_has_resource();
     146}}}
     147
     148A second deferred trigger checks the same rule when requested equipment records are inserted, updated, or deleted.
     149
     150{{{
     151CREATE CONSTRAINT TRIGGER ctg_reservation_equipment_has_resource
     152AFTER INSERT OR UPDATE OR DELETE
     153ON project.reservation_equipment
     154DEFERRABLE INITIALLY DEFERRED
     155FOR EACH ROW
     156EXECUTE FUNCTION project.fn_check_reservation_equipment_resource();
     157}}}
     158
     159=== Discussion ===
     160
     161This rule is important because the conceptual model allows flexible reservations, but it does not allow empty reservations. The use of a deferred constraint trigger is appropriate because equipment-only reservations require data in both ''project.reservations'' and ''project.reservation_equipment''.
     162
     163== Data constraints requirements: Requested equipment stock availability ==
     164
     165=== Data requirements description ===
     166
     167When equipment is requested as part of a reservation, the requested quantity should not exceed the available general stock for the same equipment type during the same time interval.
     168
     169This is a complex requirement because it depends on:
     170
     171* the requested equipment type;
     172* the requested quantity;
     173* the reservation date;
     174* the start and end time;
     175* other pending or approved reservations that request the same equipment during an overlapping time interval;
     176* the available stock quantity stored in ''project.equipment''.
     177
     178=== Implementation ===
     179
     180The function ''project.fn_validate_equipment_availability'' checks whether enough equipment is available.
     181
     182{{{
     183CREATE OR REPLACE FUNCTION project.fn_validate_equipment_availability(
     184p_reservation_id INTEGER
     185)
     186RETURNS VOID AS $$
     187DECLARE
     188rec RECORD;
     189v_overlapping_quantity INTEGER;
     190BEGIN
     191FOR rec IN
     192SELECT
     193req.equipment_id,
     194e.name AS equipment_name,
     195e.stock_quantity,
     196req.requested_quantity,
     197res.reservation_date,
     198res.start_time,
     199res.end_time,
     200res.status
     201FROM project.reservation_equipment req
     202JOIN project.reservations res
     203ON req.reservation_id = res.reservation_id
     204JOIN project.equipment e
     205ON req.equipment_id = e.equipment_id
     206WHERE req.reservation_id = p_reservation_id
     207LOOP
     208IF rec.status NOT IN ('pending', 'approved') THEN
     209CONTINUE;
     210END IF;
     211
     212```
     213    SELECT COALESCE(SUM(req2.requested_quantity), 0)
     214    INTO v_overlapping_quantity
     215    FROM project.reservation_equipment req2
     216    JOIN project.reservations res2
     217        ON req2.reservation_id = res2.reservation_id
     218    WHERE req2.equipment_id = rec.equipment_id
     219      AND res2.reservation_id <> p_reservation_id
     220      AND res2.status IN ('pending', 'approved')
     221      AND res2.reservation_date = rec.reservation_date
     222      AND rec.start_time < res2.end_time
     223      AND rec.end_time > res2.start_time;
     224
     225    IF v_overlapping_quantity + rec.requested_quantity > rec.stock_quantity THEN
     226        RAISE EXCEPTION
     227            'Not enough general stock for equipment %. Requested quantity %, overlapping active quantity %, available stock %.',
     228            rec.equipment_name,
     229            rec.requested_quantity,
     230            v_overlapping_quantity,
     231            rec.stock_quantity;
     232    END IF;
     233END LOOP;
     234```
     235
     236END;
     237$$ LANGUAGE plpgsql;
     238}}}
     239
     240The validation is executed when requested equipment is inserted or updated, and also when reservation date, time, or status changes.
     241
     242{{{
     243CREATE TRIGGER trg_validate_equipment_availability_on_request
     244AFTER INSERT OR UPDATE
     245ON project.reservation_equipment
     246FOR EACH ROW
     247EXECUTE FUNCTION project.fn_trg_validate_equipment_availability();
     248
     249CREATE TRIGGER trg_validate_equipment_availability_on_reservation
     250AFTER UPDATE OF reservation_date, start_time, end_time, status
     251ON project.reservations
     252FOR EACH ROW
     253WHEN (NEW.status IN ('pending', 'approved'))
     254EXECUTE FUNCTION project.fn_trg_validate_equipment_availability();
     255}}}
     256
     257=== Discussion ===
     258
     259This requirement protects the database from accepting equipment reservations that exceed the available stock during the same period. It is implemented at database level, so the rule is enforced even if the data is inserted outside the prototype application.
     260
     261== Stored function: Approve or reject reservation ==
     262
     263=== Data requirements description ===
     264
     265The approval process should be consistent. Only users with role ''admin'' or ''approver'' should be able to approve or reject reservations. A reservation should be approved or rejected only if it is currently pending. When an approval decision is created, the reservation status should be updated consistently.
     266
     267This logic is implemented as a stored function so that the database can perform the approval operation as a single controlled action.
     268
     269=== Implementation ===
     270
     271The following stored function approves or rejects a pending reservation.
     272
     273{{{
     274CREATE OR REPLACE FUNCTION project.fn_approve_or_reject_reservation(
     275p_reservation_id INTEGER,
     276p_approver_id INTEGER,
     277p_decision project.approval_decision_domain,
     278p_note VARCHAR DEFAULT NULL
     279)
     280RETURNS TABLE (
     281out_approval_id INTEGER,
     282out_reservation_id INTEGER,
     283out_status VARCHAR,
     284out_decision VARCHAR,
     285out_decision_time TIMESTAMP
     286) AS $$
     287DECLARE
     288v_status VARCHAR(128);
     289v_role VARCHAR(128);
     290v_approval_id INTEGER;
     291v_decision_time TIMESTAMP;
     292BEGIN
     293SELECT status
     294INTO v_status
     295FROM project.reservations
     296WHERE reservation_id = p_reservation_id
     297FOR UPDATE;
     298
     299```
     300IF NOT FOUND THEN
     301    RAISE EXCEPTION 'Reservation % does not exist.', p_reservation_id;
     302END IF;
     303
     304IF v_status <> 'pending' THEN
     305    RAISE EXCEPTION
     306        'Only pending reservations can be approved or rejected. Current status is %.',
     307        v_status;
     308END IF;
     309
     310SELECT role
     311INTO v_role
     312FROM project.users
     313WHERE user_id = p_approver_id;
     314
     315IF NOT FOUND THEN
     316    RAISE EXCEPTION 'Approver user % does not exist.', p_approver_id;
     317END IF;
     318
     319IF v_role NOT IN ('admin', 'approver') THEN
     320    RAISE EXCEPTION
     321        'User % cannot approve reservations because role % is not allowed.',
     322        p_approver_id,
     323        v_role;
     324END IF;
     325
     326UPDATE project.reservations
     327SET status = p_decision::VARCHAR
     328WHERE reservation_id = p_reservation_id;
     329
     330INSERT INTO project.approvals (
     331    reservation_id,
     332    approver_id,
     333    decision,
     334    decision_time,
     335    note
     336)
     337VALUES (
     338    p_reservation_id,
     339    p_approver_id,
     340    p_decision::VARCHAR,
     341    CURRENT_TIMESTAMP,
     342    p_note
     343)
     344RETURNING approval_id, decision_time
     345INTO v_approval_id, v_decision_time;
     346
     347RETURN QUERY
     348SELECT
     349    v_approval_id,
     350    p_reservation_id,
     351    p_decision::VARCHAR,
     352    p_decision::VARCHAR,
     353    v_decision_time;
     354```
     355
     356END;
     357$$ LANGUAGE plpgsql;
     358}}}
     359
     360=== Test example ===
     361
     362First, pending reservations can be listed through the view:
     363
     364{{{
     365SELECT *
     366FROM project.v_pending_reservation_details;
     367}}}
     368
     369Then the stored function can be executed:
     370
     371{{{
     372SELECT *
     373FROM project.fn_approve_or_reject_reservation(
     3747,
     3753,
     376'approved',
     377'Approved through P7 stored function.'
     378);
     379}}}
     380
     381After execution, the result can be checked in the base tables:
     382
     383{{{
     384SELECT *
     385FROM project.reservations
     386ORDER BY reservation_id DESC;
     387
     388SELECT *
     389FROM project.approvals
     390ORDER BY approval_id DESC;
     391}}}
     392
     393== Trigger: Validate direct approval writes ==
     394
     395=== Data requirements description ===
     396
     397Even if an approval is inserted directly into the ''project.approvals'' table, the database must still check whether the approver has an appropriate role. This prevents invalid approval records from being inserted manually.
     398
     399=== Implementation ===
     400
     401The trigger ''trg_validate_approval_direct_write'' checks the role of the approver before an approval row is inserted or updated.
     402
     403{{{
     404CREATE TRIGGER trg_validate_approval_direct_write
     405BEFORE INSERT OR UPDATE
     406ON project.approvals
     407FOR EACH ROW
     408EXECUTE FUNCTION project.fn_validate_approval_direct_write();
     409}}}
     410
     411Another trigger synchronizes the reservation status after an approval is inserted.
     412
     413{{{
     414CREATE TRIGGER trg_sync_reservation_status_after_approval
     415AFTER INSERT
     416ON project.approvals
     417FOR EACH ROW
     418EXECUTE FUNCTION project.fn_sync_reservation_status_after_approval();
     419}}}
     420
     421== Custom domain: Approval decision domain ==
     422
     423=== Data requirements description ===
     424
     425The approval decision has a restricted domain of allowed values. Instead of repeating this rule only as a table-level check, a reusable PostgreSQL custom domain is created for approval decisions.
     426
     427=== Implementation ===
     428
     429{{{
     430CREATE DOMAIN project.approval_decision_domain AS VARCHAR(128)
     431CHECK (VALUE IN ('approved', 'rejected'));
     432}}}
     433
     434This custom domain is used as the type of the ''p_decision'' parameter in the stored function ''project.fn_approve_or_reject_reservation''.
     435
     436== View: Pending reservation details ==
     437
     438=== Data requirements description ===
     439
     440Approvers need a clear overview of pending reservations. The raw database tables store this information across several relations, so a view is created to present the data in a readable form.
     441
     442The view combines:
     443
     444* reservation data;
     445* requester information;
     446* room and building information;
     447* requested equipment information.
     448
     449=== Implementation ===
     450
     451{{{
     452CREATE OR REPLACE VIEW project.v_pending_reservation_details AS
     453SELECT
     454res.reservation_id,
     455u.full_name AS requester_name,
     456res.reservation_date,
     457res.start_time,
     458res.end_time,
     459COALESCE(r.room_code, 'No room') AS room_code,
     460COALESCE(b.name, 'No building') AS building_name,
     461res.status,
     462COALESCE(eq.requested_equipment, 'No equipment') AS requested_equipment
     463FROM project.reservations res
     464JOIN project.users u
     465ON res.user_id = u.user_id
     466LEFT JOIN project.rooms r
     467ON res.room_id = r.room_id
     468LEFT JOIN project.buildings b
     469ON r.building_id = b.building_id
     470LEFT JOIN (
     471SELECT
     472req.reservation_id,
     473string_agg(e.name || ' x' || req.requested_quantity, ', ' ORDER BY e.name) AS requested_equipment
     474FROM project.reservation_equipment req
     475JOIN project.equipment e
     476ON req.equipment_id = e.equipment_id
     477GROUP BY req.reservation_id
     478) eq
     479ON res.reservation_id = eq.reservation_id
     480WHERE res.status = 'pending';
     481}}}
     482
     483=== Test query ===
     484
     485{{{
     486SELECT *
     487FROM project.v_pending_reservation_details
     488ORDER BY reservation_date, start_time, reservation_id;
     489}}}
     490
     491== View: Quarterly room utilization ==
     492
     493=== Data requirements description ===
     494
     495The system also needs analytical information about room usage. A view is created to summarize room reservations by quarter, building, room, room type, and status.
     496
     497This view supports report-style analysis and can be used by administrators to see which rooms are used most frequently.
     498
     499=== Implementation ===
     500
     501{{{
     502CREATE OR REPLACE VIEW project.v_quarterly_room_utilization AS
     503WITH room_reservation_base AS (
     504SELECT
     505date_trunc('quarter', res.reservation_date)::date AS quarter_start,
     506b.name AS building_name,
     507r.room_id,
     508r.room_code,
     509r.type,
     510r.capacity,
     511res.reservation_id,
     512res.status,
     513EXTRACT(EPOCH FROM (res.end_time - res.start_time)) / 3600.0 AS reservation_hours
     514FROM project.reservations res
     515JOIN project.rooms r
     516ON res.room_id = r.room_id
     517JOIN project.buildings b
     518ON r.building_id = b.building_id
     519WHERE res.room_id IS NOT NULL
     520),
     521room_quarter_summary AS (
     522SELECT
     523quarter_start,
     524building_name,
     525room_id,
     526room_code,
     527type,
     528capacity,
     529COUNT(reservation_id) AS total_room_reservations,
     530COUNT(*) FILTER (WHERE status = 'approved') AS approved_reservations,
     531COUNT(*) FILTER (WHERE status = 'rejected') AS rejected_reservations,
     532COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled_reservations,
     533COUNT(*) FILTER (WHERE status = 'pending') AS pending_reservations,
     534ROUND(SUM(reservation_hours), 2) AS requested_hours,
     535ROUND(COALESCE(SUM(reservation_hours) FILTER (WHERE status = 'approved'), 0), 2) AS approved_hours
     536FROM room_reservation_base
     537GROUP BY
     538quarter_start,
     539building_name,
     540room_id,
     541room_code,
     542type,
     543capacity
     544)
     545SELECT
     546quarter_start,
     547building_name,
     548room_code,
     549type,
     550capacity,
     551total_room_reservations,
     552approved_reservations,
     553rejected_reservations,
     554cancelled_reservations,
     555pending_reservations,
     556requested_hours,
     557approved_hours,
     558ROUND(
     559approved_hours / NULLIF(SUM(approved_hours) OVER (PARTITION BY quarter_start), 0) * 100,
     5602
     561) AS approved_hours_share_percent,
     562DENSE_RANK() OVER (
     563PARTITION BY quarter_start
     564ORDER BY approved_hours DESC, total_room_reservations DESC, room_code
     565) AS utilization_rank,
     566CASE
     567WHEN approved_hours >= 4 THEN 'high_usage'
     568WHEN approved_hours >= 2 THEN 'medium_usage'
     569WHEN approved_hours > 0 THEN 'low_usage'
     570ELSE 'no_approved_usage'
     571END AS utilization_level
     572FROM room_quarter_summary;
     573}}}
     574
     575=== Test query ===
     576
     577{{{
     578SELECT *
     579FROM project.v_quarterly_room_utilization
     580ORDER BY quarter_start, utilization_rank, building_name, room_code;
     581}}}
     582
     583== Final discussion ==
     584
     585The implemented database objects enforce important business rules directly inside the database. This improves the reliability of the system because the rules are checked even when data is inserted or changed outside the Java prototype application.
     586
     587The most important improvements are:
     588
     589* overlapping active room reservations are prevented;
     590* empty reservations without both room and equipment are prevented;
     591* requested equipment quantities are checked against available stock during overlapping time intervals;
     592* approval decisions are restricted to authorized users;
     593* approval processing is centralized through a stored function;
     594* pending reservations and room utilization can be accessed through reusable views;
     595* a custom domain is used for approval decisions.
     596
     597These additions are more advanced than the basic constraints from Phase P2 because they require triggers, stored functions, cross-table checks, aggregation, and procedural database logic.