AdvancedDatabaseDevelopment: advanced_database_development.sql

File advanced_database_development.sql, 14.5 KB (added by 223091, 8 days ago)
Line 
1SET search_path TO project;
2
3DO $$
4BEGIN
5 IF NOT EXISTS (
6 SELECT 1
7 FROM pg_type t
8 JOIN pg_namespace n ON n.oid = t.typnamespace
9 WHERE n.nspname = 'project'
10 AND t.typname = 'approval_decision_domain'
11 ) THEN
12 CREATE DOMAIN project.approval_decision_domain AS VARCHAR(128)
13 CHECK (VALUE IN ('approved', 'rejected'));
14 END IF;
15END $$;
16
17DROP VIEW IF EXISTS project.v_quarterly_room_utilization;
18DROP VIEW IF EXISTS project.v_pending_reservation_details;
19
20DROP TRIGGER IF EXISTS trg_prevent_room_overlap ON project.reservations;
21DROP TRIGGER IF EXISTS ctg_reservation_has_resource ON project.reservations;
22DROP TRIGGER IF EXISTS ctg_reservation_equipment_has_resource ON project.reservation_equipment;
23DROP TRIGGER IF EXISTS trg_validate_equipment_availability_on_request ON project.reservation_equipment;
24DROP TRIGGER IF EXISTS trg_validate_equipment_availability_on_reservation ON project.reservations;
25DROP TRIGGER IF EXISTS trg_validate_approval_direct_write ON project.approvals;
26DROP TRIGGER IF EXISTS trg_sync_reservation_status_after_approval ON project.approvals;
27
28DROP FUNCTION IF EXISTS project.fn_prevent_room_overlap();
29DROP FUNCTION IF EXISTS project.fn_check_reservation_has_resource();
30DROP FUNCTION IF EXISTS project.fn_check_reservation_equipment_resource();
31DROP FUNCTION IF EXISTS project.fn_validate_equipment_availability(INTEGER);
32DROP FUNCTION IF EXISTS project.fn_trg_validate_equipment_availability();
33DROP FUNCTION IF EXISTS project.fn_validate_approval_direct_write();
34DROP FUNCTION IF EXISTS project.fn_sync_reservation_status_after_approval();
35DROP FUNCTION IF EXISTS project.fn_approve_or_reject_reservation(
36 INTEGER,
37 INTEGER,
38 project.approval_decision_domain,
39 VARCHAR
40);
41
42CREATE OR REPLACE FUNCTION project.fn_prevent_room_overlap()
43RETURNS TRIGGER AS $$
44BEGIN
45 IF NEW.room_id IS NULL OR NEW.status NOT IN ('pending', 'approved') THEN
46 RETURN NEW;
47 END IF;
48
49 IF EXISTS (
50 SELECT 1
51 FROM project.reservations r
52 WHERE r.room_id = NEW.room_id
53 AND r.reservation_date = NEW.reservation_date
54 AND r.status IN ('pending', 'approved')
55 AND r.reservation_id <> COALESCE(NEW.reservation_id, -1)
56 AND NEW.start_time < r.end_time
57 AND NEW.end_time > r.start_time
58 ) THEN
59 RAISE EXCEPTION
60 'Room reservation overlap detected for room_id %, date %, time interval % - %.',
61 NEW.room_id,
62 NEW.reservation_date,
63 NEW.start_time,
64 NEW.end_time;
65 END IF;
66
67 RETURN NEW;
68END;
69$$ LANGUAGE plpgsql;
70
71CREATE TRIGGER trg_prevent_room_overlap
72BEFORE INSERT OR UPDATE OF room_id, reservation_date, start_time, end_time, status
73ON project.reservations
74FOR EACH ROW
75EXECUTE FUNCTION project.fn_prevent_room_overlap();
76
77CREATE OR REPLACE FUNCTION project.fn_check_reservation_has_resource()
78RETURNS TRIGGER AS $$
79DECLARE
80 v_has_equipment BOOLEAN;
81BEGIN
82 SELECT EXISTS (
83 SELECT 1
84 FROM project.reservation_equipment re
85 WHERE re.reservation_id = NEW.reservation_id
86 )
87 INTO v_has_equipment;
88
89 IF NEW.room_id IS NULL AND NOT v_has_equipment THEN
90 RAISE EXCEPTION
91 'Reservation % must include at least one resource: either a room or requested equipment.',
92 NEW.reservation_id;
93 END IF;
94
95 RETURN NEW;
96END;
97$$ LANGUAGE plpgsql;
98
99CREATE CONSTRAINT TRIGGER ctg_reservation_has_resource
100AFTER INSERT OR UPDATE OF room_id
101ON project.reservations
102DEFERRABLE INITIALLY DEFERRED
103FOR EACH ROW
104EXECUTE FUNCTION project.fn_check_reservation_has_resource();
105
106CREATE OR REPLACE FUNCTION project.fn_check_reservation_equipment_resource()
107RETURNS TRIGGER AS $$
108DECLARE
109 v_reservation_id INTEGER;
110 v_room_id INTEGER;
111 v_equipment_count INTEGER;
112BEGIN
113 v_reservation_id := COALESCE(NEW.reservation_id, OLD.reservation_id);
114
115 SELECT room_id
116 INTO v_room_id
117 FROM project.reservations
118 WHERE reservation_id = v_reservation_id;
119
120 IF NOT FOUND THEN
121 RETURN COALESCE(NEW, OLD);
122 END IF;
123
124 SELECT COUNT(*)
125 INTO v_equipment_count
126 FROM project.reservation_equipment
127 WHERE reservation_id = v_reservation_id;
128
129 IF v_room_id IS NULL AND v_equipment_count = 0 THEN
130 RAISE EXCEPTION
131 'Reservation % cannot remain without both room and requested equipment.',
132 v_reservation_id;
133 END IF;
134
135 RETURN COALESCE(NEW, OLD);
136END;
137$$ LANGUAGE plpgsql;
138
139CREATE CONSTRAINT TRIGGER ctg_reservation_equipment_has_resource
140AFTER INSERT OR UPDATE OR DELETE
141ON project.reservation_equipment
142DEFERRABLE INITIALLY DEFERRED
143FOR EACH ROW
144EXECUTE FUNCTION project.fn_check_reservation_equipment_resource();
145
146CREATE OR REPLACE FUNCTION project.fn_validate_equipment_availability(
147 p_reservation_id INTEGER
148)
149RETURNS VOID AS $$
150DECLARE
151 rec RECORD;
152 v_overlapping_quantity INTEGER;
153BEGIN
154 FOR rec IN
155 SELECT
156 req.equipment_id,
157 e.name AS equipment_name,
158 e.stock_quantity,
159 req.requested_quantity,
160 res.reservation_date,
161 res.start_time,
162 res.end_time,
163 res.status
164 FROM project.reservation_equipment req
165 JOIN project.reservations res
166 ON req.reservation_id = res.reservation_id
167 JOIN project.equipment e
168 ON req.equipment_id = e.equipment_id
169 WHERE req.reservation_id = p_reservation_id
170 LOOP
171 IF rec.status NOT IN ('pending', 'approved') THEN
172 CONTINUE;
173 END IF;
174
175 SELECT COALESCE(SUM(req2.requested_quantity), 0)
176 INTO v_overlapping_quantity
177 FROM project.reservation_equipment req2
178 JOIN project.reservations res2
179 ON req2.reservation_id = res2.reservation_id
180 WHERE req2.equipment_id = rec.equipment_id
181 AND res2.reservation_id <> p_reservation_id
182 AND res2.status IN ('pending', 'approved')
183 AND res2.reservation_date = rec.reservation_date
184 AND rec.start_time < res2.end_time
185 AND rec.end_time > res2.start_time;
186
187 IF v_overlapping_quantity + rec.requested_quantity > rec.stock_quantity THEN
188 RAISE EXCEPTION
189 'Not enough general stock for equipment %. Requested quantity %, overlapping active quantity %, available stock %.',
190 rec.equipment_name,
191 rec.requested_quantity,
192 v_overlapping_quantity,
193 rec.stock_quantity;
194 END IF;
195 END LOOP;
196END;
197$$ LANGUAGE plpgsql;
198
199CREATE OR REPLACE FUNCTION project.fn_trg_validate_equipment_availability()
200RETURNS TRIGGER AS $$
201BEGIN
202 PERFORM project.fn_validate_equipment_availability(
203 COALESCE(NEW.reservation_id, OLD.reservation_id)
204 );
205
206 RETURN COALESCE(NEW, OLD);
207END;
208$$ LANGUAGE plpgsql;
209
210CREATE TRIGGER trg_validate_equipment_availability_on_request
211AFTER INSERT OR UPDATE
212ON project.reservation_equipment
213FOR EACH ROW
214EXECUTE FUNCTION project.fn_trg_validate_equipment_availability();
215
216CREATE TRIGGER trg_validate_equipment_availability_on_reservation
217AFTER UPDATE OF reservation_date, start_time, end_time, status
218ON project.reservations
219FOR EACH ROW
220WHEN (NEW.status IN ('pending', 'approved'))
221EXECUTE FUNCTION project.fn_trg_validate_equipment_availability();
222
223CREATE OR REPLACE FUNCTION project.fn_validate_approval_direct_write()
224RETURNS TRIGGER AS $$
225DECLARE
226 v_role VARCHAR(128);
227 v_status VARCHAR(128);
228BEGIN
229 SELECT role
230 INTO v_role
231 FROM project.users
232 WHERE user_id = NEW.approver_id;
233
234 IF NOT FOUND THEN
235 RAISE EXCEPTION 'Approver user % does not exist.', NEW.approver_id;
236 END IF;
237
238 IF v_role NOT IN ('admin', 'approver') THEN
239 RAISE EXCEPTION
240 'User % cannot approve reservations because role % is not allowed.',
241 NEW.approver_id,
242 v_role;
243 END IF;
244
245 SELECT status
246 INTO v_status
247 FROM project.reservations
248 WHERE reservation_id = NEW.reservation_id;
249
250 IF NOT FOUND THEN
251 RAISE EXCEPTION 'Reservation % does not exist.', NEW.reservation_id;
252 END IF;
253
254 IF v_status NOT IN ('pending', NEW.decision) THEN
255 RAISE EXCEPTION
256 'Approval can be created only for pending reservations or reservations already synchronized with the same decision. Current status is %.',
257 v_status;
258 END IF;
259
260 RETURN NEW;
261END;
262$$ LANGUAGE plpgsql;
263
264CREATE TRIGGER trg_validate_approval_direct_write
265BEFORE INSERT OR UPDATE
266ON project.approvals
267FOR EACH ROW
268EXECUTE FUNCTION project.fn_validate_approval_direct_write();
269
270CREATE OR REPLACE FUNCTION project.fn_sync_reservation_status_after_approval()
271RETURNS TRIGGER AS $$
272BEGIN
273 UPDATE project.reservations
274 SET status = NEW.decision
275 WHERE reservation_id = NEW.reservation_id;
276
277 RETURN NEW;
278END;
279$$ LANGUAGE plpgsql;
280
281CREATE TRIGGER trg_sync_reservation_status_after_approval
282AFTER INSERT
283ON project.approvals
284FOR EACH ROW
285EXECUTE FUNCTION project.fn_sync_reservation_status_after_approval();
286
287CREATE OR REPLACE FUNCTION project.fn_approve_or_reject_reservation(
288 p_reservation_id INTEGER,
289 p_approver_id INTEGER,
290 p_decision project.approval_decision_domain,
291 p_note VARCHAR DEFAULT NULL
292)
293RETURNS TABLE (
294 out_approval_id INTEGER,
295 out_reservation_id INTEGER,
296 out_status VARCHAR,
297 out_decision VARCHAR,
298 out_decision_time TIMESTAMP
299) AS $$
300DECLARE
301 v_status VARCHAR(128);
302 v_role VARCHAR(128);
303 v_approval_id INTEGER;
304 v_decision_time TIMESTAMP;
305BEGIN
306 SELECT status
307 INTO v_status
308 FROM project.reservations
309 WHERE reservation_id = p_reservation_id
310 FOR UPDATE;
311
312 IF NOT FOUND THEN
313 RAISE EXCEPTION 'Reservation % does not exist.', p_reservation_id;
314 END IF;
315
316 IF v_status <> 'pending' THEN
317 RAISE EXCEPTION
318 'Only pending reservations can be approved or rejected. Current status is %.',
319 v_status;
320 END IF;
321
322 SELECT role
323 INTO v_role
324 FROM project.users
325 WHERE user_id = p_approver_id;
326
327 IF NOT FOUND THEN
328 RAISE EXCEPTION 'Approver user % does not exist.', p_approver_id;
329 END IF;
330
331 IF v_role NOT IN ('admin', 'approver') THEN
332 RAISE EXCEPTION
333 'User % cannot approve reservations because role % is not allowed.',
334 p_approver_id,
335 v_role;
336 END IF;
337
338 UPDATE project.reservations
339 SET status = p_decision::VARCHAR
340 WHERE reservation_id = p_reservation_id;
341
342 INSERT INTO project.approvals (
343 reservation_id,
344 approver_id,
345 decision,
346 decision_time,
347 note
348 )
349 VALUES (
350 p_reservation_id,
351 p_approver_id,
352 p_decision::VARCHAR,
353 CURRENT_TIMESTAMP,
354 p_note
355 )
356 RETURNING approval_id, decision_time
357 INTO v_approval_id, v_decision_time;
358
359 RETURN QUERY
360 SELECT
361 v_approval_id,
362 p_reservation_id,
363 p_decision::VARCHAR,
364 p_decision::VARCHAR,
365 v_decision_time;
366END;
367$$ LANGUAGE plpgsql;
368
369CREATE OR REPLACE VIEW project.v_pending_reservation_details AS
370SELECT
371 res.reservation_id,
372 u.full_name AS requester_name,
373 res.reservation_date,
374 res.start_time,
375 res.end_time,
376 COALESCE(r.room_code, 'No room') AS room_code,
377 COALESCE(b.name, 'No building') AS building_name,
378 res.status,
379 COALESCE(eq.requested_equipment, 'No equipment') AS requested_equipment
380FROM project.reservations res
381JOIN project.users u
382 ON res.user_id = u.user_id
383LEFT JOIN project.rooms r
384 ON res.room_id = r.room_id
385LEFT JOIN project.buildings b
386 ON r.building_id = b.building_id
387LEFT JOIN (
388 SELECT
389 req.reservation_id,
390 string_agg(e.name || ' x' || req.requested_quantity, ', ' ORDER BY e.name) AS requested_equipment
391 FROM project.reservation_equipment req
392 JOIN project.equipment e
393 ON req.equipment_id = e.equipment_id
394 GROUP BY req.reservation_id
395) eq
396 ON res.reservation_id = eq.reservation_id
397WHERE res.status = 'pending';
398
399CREATE OR REPLACE VIEW project.v_quarterly_room_utilization AS
400WITH room_reservation_base AS (
401 SELECT
402 date_trunc('quarter', res.reservation_date)::date AS quarter_start,
403 b.name AS building_name,
404 r.room_id,
405 r.room_code,
406 r.type,
407 r.capacity,
408 res.reservation_id,
409 res.status,
410 EXTRACT(EPOCH FROM (res.end_time - res.start_time)) / 3600.0 AS reservation_hours
411 FROM project.reservations res
412 JOIN project.rooms r
413 ON res.room_id = r.room_id
414 JOIN project.buildings b
415 ON r.building_id = b.building_id
416 WHERE res.room_id IS NOT NULL
417),
418room_quarter_summary AS (
419 SELECT
420 quarter_start,
421 building_name,
422 room_id,
423 room_code,
424 type,
425 capacity,
426 COUNT(reservation_id) AS total_room_reservations,
427 COUNT(*) FILTER (WHERE status = 'approved') AS approved_reservations,
428 COUNT(*) FILTER (WHERE status = 'rejected') AS rejected_reservations,
429 COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled_reservations,
430 COUNT(*) FILTER (WHERE status = 'pending') AS pending_reservations,
431 ROUND(SUM(reservation_hours), 2) AS requested_hours,
432 ROUND(COALESCE(SUM(reservation_hours) FILTER (WHERE status = 'approved'), 0), 2) AS approved_hours
433 FROM room_reservation_base
434 GROUP BY
435 quarter_start,
436 building_name,
437 room_id,
438 room_code,
439 type,
440 capacity
441)
442SELECT
443 quarter_start,
444 building_name,
445 room_code,
446 type,
447 capacity,
448 total_room_reservations,
449 approved_reservations,
450 rejected_reservations,
451 cancelled_reservations,
452 pending_reservations,
453 requested_hours,
454 approved_hours,
455 ROUND(
456 approved_hours / NULLIF(SUM(approved_hours) OVER (PARTITION BY quarter_start), 0) * 100,
457 2
458 ) AS approved_hours_share_percent,
459 DENSE_RANK() OVER (
460 PARTITION BY quarter_start
461 ORDER BY approved_hours DESC, total_room_reservations DESC, room_code
462 ) AS utilization_rank,
463 CASE
464 WHEN approved_hours >= 4 THEN 'high_usage'
465 WHEN approved_hours >= 2 THEN 'medium_usage'
466 WHEN approved_hours > 0 THEN 'low_usage'
467 ELSE 'no_approved_usage'
468 END AS utilization_level
469FROM room_quarter_summary;