DatabaseProgramming: triggers.sql

File triggers.sql, 9.0 KB (added by 231082, 4 days ago)

Triggers.

Line 
1CREATE OR REPLACE FUNCTION fn_trg_academic_program_defaults()
2 RETURNS trigger LANGUAGE plpgsql AS $$
3BEGIN
4
5 IF NEW.updated_at IS NULL THEN
6 NEW.updated_at := NEW.created_at;
7 END IF;
8
9 IF TG_OP = 'UPDATE' AND NEW.updated_at < OLD.updated_at THEN
10 RAISE EXCEPTION
11 'updated_at (%) cannot be set earlier than current updated_at (%).',
12 NEW.updated_at, OLD.updated_at;
13 END IF;
14 RETURN NEW;
15END;
16$$;
17
18CREATE TRIGGER trg_academic_program_defaults
19 BEFORE INSERT OR UPDATE ON Academic_Program
20 FOR EACH ROW
21EXECUTE FUNCTION fn_trg_academic_program_defaults();
22
23
24
25
26
27
28CREATE OR REPLACE FUNCTION fn_trg_prevent_prereq_cycle()
29 RETURNS trigger LANGUAGE plpgsql AS $$
30DECLARE
31 v_cycle_exists boolean;
32BEGIN
33
34
35 WITH RECURSIVE reachable AS (
36
37 SELECT successor_course_code AS node
38 FROM Course_Prerequisite
39 WHERE predecessor_course_code = NEW.successor_course_code
40
41 UNION
42
43 SELECT cp.successor_course_code
44 FROM Course_Prerequisite cp
45 JOIN reachable r ON r.node = cp.predecessor_course_code
46 )
47 SELECT EXISTS (
48 SELECT 1 FROM reachable WHERE node = NEW.predecessor_course_code
49 ) INTO v_cycle_exists;
50
51 IF v_cycle_exists THEN
52 RAISE EXCEPTION
53 'Adding prerequisite % -> % would create a circular dependency.',
54 NEW.predecessor_course_code, NEW.successor_course_code;
55 END IF;
56
57 RETURN NEW;
58END;
59$$;
60
61CREATE TRIGGER trg_prevent_prereq_cycle
62 BEFORE INSERT OR UPDATE ON Course_Prerequisite
63 FOR EACH ROW
64EXECUTE FUNCTION fn_trg_prevent_prereq_cycle();
65
66
67
68
69
70
71
72
73
74
75CREATE OR REPLACE FUNCTION fn_trg_prevent_equiv_duplicate()
76 RETURNS trigger LANGUAGE plpgsql AS $$
77BEGIN
78
79 IF EXISTS (
80 SELECT 1 FROM Course_Equivalence
81 WHERE course1_code = NEW.course2_code
82 AND course2_code = NEW.course1_code
83 ) THEN
84 RAISE EXCEPTION
85 'Equivalence (%, %) already exists as (%, %) — reverse pair not allowed.',
86 NEW.course1_code, NEW.course2_code,
87 NEW.course2_code, NEW.course1_code;
88 END IF;
89 RETURN NEW;
90END;
91$$;
92
93CREATE TRIGGER trg_prevent_equiv_duplicate
94 BEFORE INSERT ON Course_Equivalence
95 FOR EACH ROW
96EXECUTE FUNCTION fn_trg_prevent_equiv_duplicate();
97
98
99
100
101
102CREATE OR REPLACE FUNCTION fn_trg_validate_submission_grade()
103 RETURNS trigger LANGUAGE plpgsql AS $$
104DECLARE
105 v_max_grade integer;
106BEGIN
107
108 IF NEW.grade IS NULL THEN
109 RETURN NEW;
110 END IF;
111
112 SELECT max_grade INTO v_max_grade
113 FROM Exercise
114 WHERE id = NEW.exercise_id;
115
116 IF NEW.grade > v_max_grade THEN
117 RAISE EXCEPTION
118 'Submission grade (%) exceeds exercise max_grade (%) for exercise %.',
119 NEW.grade, v_max_grade, NEW.exercise_id;
120 END IF;
121
122 IF NEW.grade < 0 THEN
123 RAISE EXCEPTION
124 'Submission grade (%) cannot be negative.', NEW.grade;
125 END IF;
126
127 RETURN NEW;
128END;
129$$;
130
131CREATE TRIGGER trg_validate_submission_grade
132 BEFORE INSERT OR UPDATE ON Exercise_Submission
133 FOR EACH ROW
134EXECUTE FUNCTION fn_trg_validate_submission_grade();
135
136
137
138
139
140CREATE OR REPLACE FUNCTION fn_trg_validate_survey_response_option()
141 RETURNS trigger LANGUAGE plpgsql AS $$
142BEGIN
143 IF NOT EXISTS (
144 SELECT 1 FROM Survey_Option
145 WHERE survey_id = NEW.survey_id
146 AND option = NEW.option_text
147 ) THEN
148 RAISE EXCEPTION
149 'Response option "%" is not a valid option for survey %.',
150 NEW.option_text, NEW.survey_id;
151 END IF;
152 RETURN NEW;
153END;
154$$;
155
156CREATE TRIGGER trg_validate_survey_response_option
157 BEFORE INSERT OR UPDATE ON Survey_Response
158 FOR EACH ROW
159EXECUTE FUNCTION fn_trg_validate_survey_response_option();
160
161
162
163
164
165CREATE OR REPLACE FUNCTION fn_trg_validate_survey_response_timing()
166 RETURNS trigger LANGUAGE plpgsql AS $$
167DECLARE
168 v_open_until timestamp without time zone;
169BEGIN
170 SELECT open_until INTO v_open_until
171 FROM Survey
172 WHERE id = NEW.survey_id;
173
174
175 IF v_open_until IS NOT NULL AND NEW.responded_at > v_open_until THEN
176 RAISE EXCEPTION
177 'Survey % closed at %. Response submitted at % is not allowed.',
178 NEW.survey_id, v_open_until, NEW.responded_at;
179 END IF;
180
181 RETURN NEW;
182END;
183$$;
184
185CREATE TRIGGER trg_validate_survey_response_timing
186 BEFORE INSERT ON Survey_Response
187 FOR EACH ROW
188EXECUTE FUNCTION fn_trg_validate_survey_response_timing();
189
190
191
192
193
194CREATE OR REPLACE FUNCTION fn_trg_validate_exam_attempt_timing()
195 RETURNS trigger LANGUAGE plpgsql AS $$
196DECLARE
197 v_scheduled_at timestamp without time zone;
198 v_finishes_at timestamp without time zone;
199BEGIN
200 SELECT
201 scheduled_at,
202 scheduled_at + (duration_minutes * INTERVAL '1 minute')
203 INTO v_scheduled_at, v_finishes_at
204 FROM Exam
205 WHERE id = NEW.exam_id;
206
207
208 IF NEW.submitted_at < v_scheduled_at THEN
209 RAISE EXCEPTION
210 'Exam attempt submitted_at (%) is before exam scheduled_at (%) for exam %.',
211 NEW.submitted_at, v_scheduled_at, NEW.exam_id;
212 END IF;
213
214
215
216 IF NEW.submitted_at > v_finishes_at + INTERVAL '5 minutes' THEN
217 RAISE EXCEPTION
218 'Exam attempt submitted_at (%) is more than 5 minutes after exam end (%) for exam %.',
219 NEW.submitted_at, v_finishes_at, NEW.exam_id;
220 END IF;
221
222 RETURN NEW;
223END;
224$$;
225
226CREATE TRIGGER trg_validate_exam_attempt_timing
227 BEFORE INSERT OR UPDATE ON Exam_Attempt
228 FOR EACH ROW
229EXECUTE FUNCTION fn_trg_validate_exam_attempt_timing();
230
231
232
233
234
235CREATE OR REPLACE FUNCTION fn_trg_validate_course_edition_year()
236 RETURNS trigger LANGUAGE plpgsql AS $$
237DECLARE
238 v_course_created_at date;
239 v_current_year smallint;
240 v_edition_start date;
241BEGIN
242 v_current_year := EXTRACT(YEAR FROM CURRENT_DATE)::smallint;
243
244
245
246 IF NEW.academic_year > v_current_year THEN
247 RAISE EXCEPTION
248 'Course edition academic_year (%) is in the future. Current year: %.',
249 NEW.academic_year, v_current_year;
250 END IF;
251
252
253 SELECT created_at INTO v_course_created_at
254 FROM Course
255 WHERE code = NEW.course_code;
256
257
258 v_edition_start := CASE NEW.semester::int
259 WHEN 1 THEN make_date(NEW.academic_year::int, 10, 1)
260 WHEN 2 THEN make_date(NEW.academic_year::int + 1, 2, 15)
261 END;
262
263 IF v_edition_start < v_course_created_at THEN
264 RAISE EXCEPTION
265 'Course edition for % (starting %) cannot be before course created_at (%).',
266 NEW.course_code, v_edition_start, v_course_created_at;
267 END IF;
268
269 RETURN NEW;
270END;
271$$;
272
273CREATE TRIGGER trg_validate_course_edition_year
274 BEFORE INSERT OR UPDATE ON Course_Edition
275 FOR EACH ROW
276EXECUTE FUNCTION fn_trg_validate_course_edition_year();
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294CREATE OR REPLACE FUNCTION fn_trg_sync_student_grades()
295 RETURNS trigger
296 LANGUAGE plpgsql AS $$
297DECLARE
298 v_elem jsonb;
299 v_student_id integer;
300 v_ce_id integer;
301 v_grade integer;
302 v_passed boolean;
303BEGIN
304
305 FOR v_elem IN SELECT * FROM jsonb_array_elements(NEW.data) LOOP
306
307 v_passed := (v_elem->>'passed')::boolean;
308 v_grade := CASE
309 WHEN v_elem->'grade' IS NOT NULL
310 AND jsonb_typeof(v_elem->'grade') <> 'null'
311 THEN (v_elem->>'grade')::integer
312 ELSE NULL
313 END;
314
315
316 CONTINUE WHEN NOT v_passed OR v_grade IS NULL;
317
318
319 SELECT id INTO v_student_id
320 FROM Student
321 WHERE index = v_elem->>'index';
322
323 IF v_student_id IS NULL THEN
324
325 CONTINUE;
326 END IF;
327
328
329 SELECT ce.id INTO v_ce_id
330 FROM Course_Enrollment ce
331 JOIN Semester_Enrollment se ON se.id = ce.semester_enrollment_id
332 JOIN Program_Enrollment pe ON pe.id = se.program_enrollment_id
333 WHERE pe.student_id = v_student_id
334 AND ce.course_edition_id = NEW.teaches_course_edition_id
335
336 ORDER BY se.academic_year DESC, se.semester::int DESC
337 LIMIT 1;
338
339 IF v_ce_id IS NULL THEN
340 CONTINUE;
341 END IF;
342
343
344 INSERT INTO Student_Grade (
345 course_enrollment_id,
346 graded_at,
347 grade,
348 aggr_course_edition_res_id
349 )
350 VALUES (
351 v_ce_id,
352 CURRENT_DATE,
353 v_grade::char,
354 NEW.id
355 )
356 ON CONFLICT (course_enrollment_id) DO UPDATE
357 SET grade = EXCLUDED.grade,
358 graded_at = EXCLUDED.graded_at,
359 aggr_course_edition_res_id = EXCLUDED.aggr_course_edition_res_id
360
361
362 WHERE Student_Grade.grade::int <= EXCLUDED.grade::int;
363
364 END LOOP;
365
366 RETURN NEW;
367END;
368$$;
369
370CREATE TRIGGER trg_sync_student_grades
371 AFTER INSERT
372 ON Aggregated_Course_Edition_Results
373 FOR EACH ROW
374EXECUTE FUNCTION fn_trg_sync_student_grades();