| | 1 | = Advanced Database Development = |
| | 2 | |
| | 3 | This 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 | |
| | 5 | The 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 | |
| | 7 | The 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 | |
| | 15 | A 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 | |
| | 17 | The 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 | |
| | 19 | This 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 | |
| | 23 | The following trigger function checks whether a new or updated reservation overlaps with an existing active room reservation. |
| | 24 | |
| | 25 | {{{ |
| | 26 | CREATE OR REPLACE FUNCTION project.fn_prevent_room_overlap() |
| | 27 | RETURNS TRIGGER AS $$ |
| | 28 | BEGIN |
| | 29 | IF NEW.room_id IS NULL OR NEW.status NOT IN ('pending', 'approved') THEN |
| | 30 | RETURN NEW; |
| | 31 | END IF; |
| | 32 | |
| | 33 | ``` |
| | 34 | IF 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; |
| | 50 | END IF; |
| | 51 | |
| | 52 | RETURN NEW; |
| | 53 | ``` |
| | 54 | |
| | 55 | END; |
| | 56 | $$ LANGUAGE plpgsql; |
| | 57 | }}} |
| | 58 | |
| | 59 | The trigger is executed before inserting or updating a reservation. |
| | 60 | |
| | 61 | {{{ |
| | 62 | CREATE TRIGGER trg_prevent_room_overlap |
| | 63 | BEFORE INSERT OR UPDATE OF room_id, reservation_date, start_time, end_time, status |
| | 64 | ON project.reservations |
| | 65 | FOR EACH ROW |
| | 66 | EXECUTE FUNCTION project.fn_prevent_room_overlap(); |
| | 67 | }}} |
| | 68 | |
| | 69 | === Test example === |
| | 70 | |
| | 71 | {{{ |
| | 72 | INSERT INTO project.reservations ( |
| | 73 | room_id, |
| | 74 | user_id, |
| | 75 | reservation_date, |
| | 76 | start_time, |
| | 77 | end_time, |
| | 78 | status |
| | 79 | ) |
| | 80 | VALUES ( |
| | 81 | 3, |
| | 82 | 1, |
| | 83 | DATE '2026-03-01', |
| | 84 | TIME '09:30', |
| | 85 | TIME '10:30', |
| | 86 | 'pending' |
| | 87 | ); |
| | 88 | }}} |
| | 89 | |
| | 90 | If 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 | |
| | 96 | A reservation may be created for: |
| | 97 | |
| | 98 | * only a room; |
| | 99 | * only equipment; |
| | 100 | * both a room and equipment. |
| | 101 | |
| | 102 | However, 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 | |
| | 104 | Therefore, 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 | |
| | 108 | The trigger function checks whether the reservation has either a room or at least one requested equipment item. |
| | 109 | |
| | 110 | {{{ |
| | 111 | CREATE OR REPLACE FUNCTION project.fn_check_reservation_has_resource() |
| | 112 | RETURNS TRIGGER AS $$ |
| | 113 | DECLARE |
| | 114 | v_has_equipment BOOLEAN; |
| | 115 | BEGIN |
| | 116 | SELECT EXISTS ( |
| | 117 | SELECT 1 |
| | 118 | FROM project.reservation_equipment re |
| | 119 | WHERE re.reservation_id = NEW.reservation_id |
| | 120 | ) |
| | 121 | INTO v_has_equipment; |
| | 122 | |
| | 123 | ``` |
| | 124 | IF 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; |
| | 128 | END IF; |
| | 129 | |
| | 130 | RETURN NEW; |
| | 131 | ``` |
| | 132 | |
| | 133 | END; |
| | 134 | $$ LANGUAGE plpgsql; |
| | 135 | }}} |
| | 136 | |
| | 137 | The trigger is created as a deferred constraint trigger. |
| | 138 | |
| | 139 | {{{ |
| | 140 | CREATE CONSTRAINT TRIGGER ctg_reservation_has_resource |
| | 141 | AFTER INSERT OR UPDATE OF room_id |
| | 142 | ON project.reservations |
| | 143 | DEFERRABLE INITIALLY DEFERRED |
| | 144 | FOR EACH ROW |
| | 145 | EXECUTE FUNCTION project.fn_check_reservation_has_resource(); |
| | 146 | }}} |
| | 147 | |
| | 148 | A second deferred trigger checks the same rule when requested equipment records are inserted, updated, or deleted. |
| | 149 | |
| | 150 | {{{ |
| | 151 | CREATE CONSTRAINT TRIGGER ctg_reservation_equipment_has_resource |
| | 152 | AFTER INSERT OR UPDATE OR DELETE |
| | 153 | ON project.reservation_equipment |
| | 154 | DEFERRABLE INITIALLY DEFERRED |
| | 155 | FOR EACH ROW |
| | 156 | EXECUTE FUNCTION project.fn_check_reservation_equipment_resource(); |
| | 157 | }}} |
| | 158 | |
| | 159 | === Discussion === |
| | 160 | |
| | 161 | This 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 | |
| | 167 | When 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 | |
| | 169 | This 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 | |
| | 180 | The function ''project.fn_validate_equipment_availability'' checks whether enough equipment is available. |
| | 181 | |
| | 182 | {{{ |
| | 183 | CREATE OR REPLACE FUNCTION project.fn_validate_equipment_availability( |
| | 184 | p_reservation_id INTEGER |
| | 185 | ) |
| | 186 | RETURNS VOID AS $$ |
| | 187 | DECLARE |
| | 188 | rec RECORD; |
| | 189 | v_overlapping_quantity INTEGER; |
| | 190 | BEGIN |
| | 191 | FOR rec IN |
| | 192 | SELECT |
| | 193 | req.equipment_id, |
| | 194 | e.name AS equipment_name, |
| | 195 | e.stock_quantity, |
| | 196 | req.requested_quantity, |
| | 197 | res.reservation_date, |
| | 198 | res.start_time, |
| | 199 | res.end_time, |
| | 200 | res.status |
| | 201 | FROM project.reservation_equipment req |
| | 202 | JOIN project.reservations res |
| | 203 | ON req.reservation_id = res.reservation_id |
| | 204 | JOIN project.equipment e |
| | 205 | ON req.equipment_id = e.equipment_id |
| | 206 | WHERE req.reservation_id = p_reservation_id |
| | 207 | LOOP |
| | 208 | IF rec.status NOT IN ('pending', 'approved') THEN |
| | 209 | CONTINUE; |
| | 210 | END 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; |
| | 233 | END LOOP; |
| | 234 | ``` |
| | 235 | |
| | 236 | END; |
| | 237 | $$ LANGUAGE plpgsql; |
| | 238 | }}} |
| | 239 | |
| | 240 | The validation is executed when requested equipment is inserted or updated, and also when reservation date, time, or status changes. |
| | 241 | |
| | 242 | {{{ |
| | 243 | CREATE TRIGGER trg_validate_equipment_availability_on_request |
| | 244 | AFTER INSERT OR UPDATE |
| | 245 | ON project.reservation_equipment |
| | 246 | FOR EACH ROW |
| | 247 | EXECUTE FUNCTION project.fn_trg_validate_equipment_availability(); |
| | 248 | |
| | 249 | CREATE TRIGGER trg_validate_equipment_availability_on_reservation |
| | 250 | AFTER UPDATE OF reservation_date, start_time, end_time, status |
| | 251 | ON project.reservations |
| | 252 | FOR EACH ROW |
| | 253 | WHEN (NEW.status IN ('pending', 'approved')) |
| | 254 | EXECUTE FUNCTION project.fn_trg_validate_equipment_availability(); |
| | 255 | }}} |
| | 256 | |
| | 257 | === Discussion === |
| | 258 | |
| | 259 | This 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 | |
| | 265 | The 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 | |
| | 267 | This 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 | |
| | 271 | The following stored function approves or rejects a pending reservation. |
| | 272 | |
| | 273 | {{{ |
| | 274 | CREATE OR REPLACE FUNCTION project.fn_approve_or_reject_reservation( |
| | 275 | p_reservation_id INTEGER, |
| | 276 | p_approver_id INTEGER, |
| | 277 | p_decision project.approval_decision_domain, |
| | 278 | p_note VARCHAR DEFAULT NULL |
| | 279 | ) |
| | 280 | RETURNS TABLE ( |
| | 281 | out_approval_id INTEGER, |
| | 282 | out_reservation_id INTEGER, |
| | 283 | out_status VARCHAR, |
| | 284 | out_decision VARCHAR, |
| | 285 | out_decision_time TIMESTAMP |
| | 286 | ) AS $$ |
| | 287 | DECLARE |
| | 288 | v_status VARCHAR(128); |
| | 289 | v_role VARCHAR(128); |
| | 290 | v_approval_id INTEGER; |
| | 291 | v_decision_time TIMESTAMP; |
| | 292 | BEGIN |
| | 293 | SELECT status |
| | 294 | INTO v_status |
| | 295 | FROM project.reservations |
| | 296 | WHERE reservation_id = p_reservation_id |
| | 297 | FOR UPDATE; |
| | 298 | |
| | 299 | ``` |
| | 300 | IF NOT FOUND THEN |
| | 301 | RAISE EXCEPTION 'Reservation % does not exist.', p_reservation_id; |
| | 302 | END IF; |
| | 303 | |
| | 304 | IF v_status <> 'pending' THEN |
| | 305 | RAISE EXCEPTION |
| | 306 | 'Only pending reservations can be approved or rejected. Current status is %.', |
| | 307 | v_status; |
| | 308 | END IF; |
| | 309 | |
| | 310 | SELECT role |
| | 311 | INTO v_role |
| | 312 | FROM project.users |
| | 313 | WHERE user_id = p_approver_id; |
| | 314 | |
| | 315 | IF NOT FOUND THEN |
| | 316 | RAISE EXCEPTION 'Approver user % does not exist.', p_approver_id; |
| | 317 | END IF; |
| | 318 | |
| | 319 | IF 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; |
| | 324 | END IF; |
| | 325 | |
| | 326 | UPDATE project.reservations |
| | 327 | SET status = p_decision::VARCHAR |
| | 328 | WHERE reservation_id = p_reservation_id; |
| | 329 | |
| | 330 | INSERT INTO project.approvals ( |
| | 331 | reservation_id, |
| | 332 | approver_id, |
| | 333 | decision, |
| | 334 | decision_time, |
| | 335 | note |
| | 336 | ) |
| | 337 | VALUES ( |
| | 338 | p_reservation_id, |
| | 339 | p_approver_id, |
| | 340 | p_decision::VARCHAR, |
| | 341 | CURRENT_TIMESTAMP, |
| | 342 | p_note |
| | 343 | ) |
| | 344 | RETURNING approval_id, decision_time |
| | 345 | INTO v_approval_id, v_decision_time; |
| | 346 | |
| | 347 | RETURN QUERY |
| | 348 | SELECT |
| | 349 | v_approval_id, |
| | 350 | p_reservation_id, |
| | 351 | p_decision::VARCHAR, |
| | 352 | p_decision::VARCHAR, |
| | 353 | v_decision_time; |
| | 354 | ``` |
| | 355 | |
| | 356 | END; |
| | 357 | $$ LANGUAGE plpgsql; |
| | 358 | }}} |
| | 359 | |
| | 360 | === Test example === |
| | 361 | |
| | 362 | First, pending reservations can be listed through the view: |
| | 363 | |
| | 364 | {{{ |
| | 365 | SELECT * |
| | 366 | FROM project.v_pending_reservation_details; |
| | 367 | }}} |
| | 368 | |
| | 369 | Then the stored function can be executed: |
| | 370 | |
| | 371 | {{{ |
| | 372 | SELECT * |
| | 373 | FROM project.fn_approve_or_reject_reservation( |
| | 374 | 7, |
| | 375 | 3, |
| | 376 | 'approved', |
| | 377 | 'Approved through P7 stored function.' |
| | 378 | ); |
| | 379 | }}} |
| | 380 | |
| | 381 | After execution, the result can be checked in the base tables: |
| | 382 | |
| | 383 | {{{ |
| | 384 | SELECT * |
| | 385 | FROM project.reservations |
| | 386 | ORDER BY reservation_id DESC; |
| | 387 | |
| | 388 | SELECT * |
| | 389 | FROM project.approvals |
| | 390 | ORDER BY approval_id DESC; |
| | 391 | }}} |
| | 392 | |
| | 393 | == Trigger: Validate direct approval writes == |
| | 394 | |
| | 395 | === Data requirements description === |
| | 396 | |
| | 397 | Even 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 | |
| | 401 | The trigger ''trg_validate_approval_direct_write'' checks the role of the approver before an approval row is inserted or updated. |
| | 402 | |
| | 403 | {{{ |
| | 404 | CREATE TRIGGER trg_validate_approval_direct_write |
| | 405 | BEFORE INSERT OR UPDATE |
| | 406 | ON project.approvals |
| | 407 | FOR EACH ROW |
| | 408 | EXECUTE FUNCTION project.fn_validate_approval_direct_write(); |
| | 409 | }}} |
| | 410 | |
| | 411 | Another trigger synchronizes the reservation status after an approval is inserted. |
| | 412 | |
| | 413 | {{{ |
| | 414 | CREATE TRIGGER trg_sync_reservation_status_after_approval |
| | 415 | AFTER INSERT |
| | 416 | ON project.approvals |
| | 417 | FOR EACH ROW |
| | 418 | EXECUTE FUNCTION project.fn_sync_reservation_status_after_approval(); |
| | 419 | }}} |
| | 420 | |
| | 421 | == Custom domain: Approval decision domain == |
| | 422 | |
| | 423 | === Data requirements description === |
| | 424 | |
| | 425 | The 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 | {{{ |
| | 430 | CREATE DOMAIN project.approval_decision_domain AS VARCHAR(128) |
| | 431 | CHECK (VALUE IN ('approved', 'rejected')); |
| | 432 | }}} |
| | 433 | |
| | 434 | This 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 | |
| | 440 | Approvers 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 | |
| | 442 | The view combines: |
| | 443 | |
| | 444 | * reservation data; |
| | 445 | * requester information; |
| | 446 | * room and building information; |
| | 447 | * requested equipment information. |
| | 448 | |
| | 449 | === Implementation === |
| | 450 | |
| | 451 | {{{ |
| | 452 | CREATE OR REPLACE VIEW project.v_pending_reservation_details AS |
| | 453 | SELECT |
| | 454 | res.reservation_id, |
| | 455 | u.full_name AS requester_name, |
| | 456 | res.reservation_date, |
| | 457 | res.start_time, |
| | 458 | res.end_time, |
| | 459 | COALESCE(r.room_code, 'No room') AS room_code, |
| | 460 | COALESCE(b.name, 'No building') AS building_name, |
| | 461 | res.status, |
| | 462 | COALESCE(eq.requested_equipment, 'No equipment') AS requested_equipment |
| | 463 | FROM project.reservations res |
| | 464 | JOIN project.users u |
| | 465 | ON res.user_id = u.user_id |
| | 466 | LEFT JOIN project.rooms r |
| | 467 | ON res.room_id = r.room_id |
| | 468 | LEFT JOIN project.buildings b |
| | 469 | ON r.building_id = b.building_id |
| | 470 | LEFT JOIN ( |
| | 471 | SELECT |
| | 472 | req.reservation_id, |
| | 473 | string_agg(e.name || ' x' || req.requested_quantity, ', ' ORDER BY e.name) AS requested_equipment |
| | 474 | FROM project.reservation_equipment req |
| | 475 | JOIN project.equipment e |
| | 476 | ON req.equipment_id = e.equipment_id |
| | 477 | GROUP BY req.reservation_id |
| | 478 | ) eq |
| | 479 | ON res.reservation_id = eq.reservation_id |
| | 480 | WHERE res.status = 'pending'; |
| | 481 | }}} |
| | 482 | |
| | 483 | === Test query === |
| | 484 | |
| | 485 | {{{ |
| | 486 | SELECT * |
| | 487 | FROM project.v_pending_reservation_details |
| | 488 | ORDER BY reservation_date, start_time, reservation_id; |
| | 489 | }}} |
| | 490 | |
| | 491 | == View: Quarterly room utilization == |
| | 492 | |
| | 493 | === Data requirements description === |
| | 494 | |
| | 495 | The 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 | |
| | 497 | This view supports report-style analysis and can be used by administrators to see which rooms are used most frequently. |
| | 498 | |
| | 499 | === Implementation === |
| | 500 | |
| | 501 | {{{ |
| | 502 | CREATE OR REPLACE VIEW project.v_quarterly_room_utilization AS |
| | 503 | WITH room_reservation_base AS ( |
| | 504 | SELECT |
| | 505 | date_trunc('quarter', res.reservation_date)::date AS quarter_start, |
| | 506 | b.name AS building_name, |
| | 507 | r.room_id, |
| | 508 | r.room_code, |
| | 509 | r.type, |
| | 510 | r.capacity, |
| | 511 | res.reservation_id, |
| | 512 | res.status, |
| | 513 | EXTRACT(EPOCH FROM (res.end_time - res.start_time)) / 3600.0 AS reservation_hours |
| | 514 | FROM project.reservations res |
| | 515 | JOIN project.rooms r |
| | 516 | ON res.room_id = r.room_id |
| | 517 | JOIN project.buildings b |
| | 518 | ON r.building_id = b.building_id |
| | 519 | WHERE res.room_id IS NOT NULL |
| | 520 | ), |
| | 521 | room_quarter_summary AS ( |
| | 522 | SELECT |
| | 523 | quarter_start, |
| | 524 | building_name, |
| | 525 | room_id, |
| | 526 | room_code, |
| | 527 | type, |
| | 528 | capacity, |
| | 529 | COUNT(reservation_id) AS total_room_reservations, |
| | 530 | COUNT(*) FILTER (WHERE status = 'approved') AS approved_reservations, |
| | 531 | COUNT(*) FILTER (WHERE status = 'rejected') AS rejected_reservations, |
| | 532 | COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled_reservations, |
| | 533 | COUNT(*) FILTER (WHERE status = 'pending') AS pending_reservations, |
| | 534 | ROUND(SUM(reservation_hours), 2) AS requested_hours, |
| | 535 | ROUND(COALESCE(SUM(reservation_hours) FILTER (WHERE status = 'approved'), 0), 2) AS approved_hours |
| | 536 | FROM room_reservation_base |
| | 537 | GROUP BY |
| | 538 | quarter_start, |
| | 539 | building_name, |
| | 540 | room_id, |
| | 541 | room_code, |
| | 542 | type, |
| | 543 | capacity |
| | 544 | ) |
| | 545 | SELECT |
| | 546 | quarter_start, |
| | 547 | building_name, |
| | 548 | room_code, |
| | 549 | type, |
| | 550 | capacity, |
| | 551 | total_room_reservations, |
| | 552 | approved_reservations, |
| | 553 | rejected_reservations, |
| | 554 | cancelled_reservations, |
| | 555 | pending_reservations, |
| | 556 | requested_hours, |
| | 557 | approved_hours, |
| | 558 | ROUND( |
| | 559 | approved_hours / NULLIF(SUM(approved_hours) OVER (PARTITION BY quarter_start), 0) * 100, |
| | 560 | 2 |
| | 561 | ) AS approved_hours_share_percent, |
| | 562 | DENSE_RANK() OVER ( |
| | 563 | PARTITION BY quarter_start |
| | 564 | ORDER BY approved_hours DESC, total_room_reservations DESC, room_code |
| | 565 | ) AS utilization_rank, |
| | 566 | CASE |
| | 567 | WHEN approved_hours >= 4 THEN 'high_usage' |
| | 568 | WHEN approved_hours >= 2 THEN 'medium_usage' |
| | 569 | WHEN approved_hours > 0 THEN 'low_usage' |
| | 570 | ELSE 'no_approved_usage' |
| | 571 | END AS utilization_level |
| | 572 | FROM room_quarter_summary; |
| | 573 | }}} |
| | 574 | |
| | 575 | === Test query === |
| | 576 | |
| | 577 | {{{ |
| | 578 | SELECT * |
| | 579 | FROM project.v_quarterly_room_utilization |
| | 580 | ORDER BY quarter_start, utilization_rank, building_name, room_code; |
| | 581 | }}} |
| | 582 | |
| | 583 | == Final discussion == |
| | 584 | |
| | 585 | The 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 | |
| | 587 | The 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 | |
| | 597 | These 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. |