DatabaseProgramming: triggers.sql

File triggers.sql, 10.6 KB (added by 235013, 11 days ago)
Line 
1-- =============================================================================
2-- TRIGGER 1-3: User Subtype Enforcement
3-- =============================================================================
4-- A row in Client_User, Vendor_User or Management_User is accepted only when
5-- "User".type names that subtype. Because the type cannot change afterwards
6-- (trg_user_type_immutable), no user ever has rows in two subtype tables.
7CREATE OR REPLACE FUNCTION trg_enforce_user_subtype()
8RETURNS TRIGGER LANGUAGE plpgsql AS $$
9DECLARE
10 v_expected text;
11 v_actual text;
12BEGIN
13 v_expected := CASE TG_TABLE_NAME
14 WHEN 'client_user' THEN 'client'
15 WHEN 'vendor_user' THEN 'vendor'
16 WHEN 'management_user' THEN 'management'
17 ELSE NULL
18 END;
19
20 IF v_expected IS NULL THEN
21 RAISE EXCEPTION 'trg_enforce_user_subtype is not valid on table %', TG_TABLE_NAME;
22 END IF;
23
24 SELECT type INTO v_actual FROM "User" WHERE user_id = NEW.user_id;
25
26 IF NOT FOUND THEN
27 RAISE EXCEPTION 'User % does not exist', NEW.user_id;
28 END IF;
29
30 IF v_actual <> v_expected THEN
31 RAISE EXCEPTION 'User % type must be % (it is %)', NEW.user_id, v_expected, v_actual;
32 END IF;
33
34 RETURN NEW;
35END;
36$$;
37
38CREATE OR REPLACE TRIGGER trg_client_user_subtype
39 BEFORE INSERT OR UPDATE ON Client_User
40 FOR EACH ROW EXECUTE FUNCTION trg_enforce_user_subtype();
41
42CREATE OR REPLACE TRIGGER trg_vendor_user_subtype
43 BEFORE INSERT OR UPDATE ON Vendor_User
44 FOR EACH ROW EXECUTE FUNCTION trg_enforce_user_subtype();
45
46CREATE OR REPLACE TRIGGER trg_management_user_subtype
47 BEFORE INSERT OR UPDATE ON Management_User
48 FOR EACH ROW EXECUTE FUNCTION trg_enforce_user_subtype();
49
50
51-- =============================================================================
52-- TRIGGER 4: User Type Immutability
53-- =============================================================================
54-- Keeps "User".type fixed, so the subtype row written for it cannot become
55-- inconsistent.
56CREATE OR REPLACE FUNCTION trg_prevent_user_type_change()
57RETURNS TRIGGER LANGUAGE plpgsql AS $$
58BEGIN
59 RAISE EXCEPTION 'User % type must stay % and cannot be changed', OLD.user_id, OLD.type;
60END;
61$$;
62
63CREATE OR REPLACE TRIGGER trg_user_type_immutable
64 BEFORE UPDATE OF type ON "User"
65 FOR EACH ROW
66 WHEN (NEW.type <> OLD.type)
67 EXECUTE FUNCTION trg_prevent_user_type_change();
68
69
70-- =============================================================================
71-- TRIGGER 5: Vendor Subscription Negotiated Price Validation
72-- =============================================================================
73CREATE OR REPLACE FUNCTION trg_enforce_negotiated_price()
74RETURNS TRIGGER LANGUAGE plpgsql AS $$
75DECLARE
76 v_allows_custom bool;
77BEGIN
78 PERFORM fn_assert_tier_exists(NEW.tier_id);
79
80 SELECT allows_custom_pricing
81 INTO v_allows_custom
82 FROM Subscription_Tier
83 WHERE tier_id = NEW.tier_id;
84
85 IF v_allows_custom AND NEW.negotiated_price IS NULL THEN
86 RAISE EXCEPTION
87 'negotiated_price is required for tiers with custom pricing (contract_id: %)', NEW.contract_id;
88 END IF;
89
90 IF NOT v_allows_custom AND NEW.negotiated_price IS NOT NULL THEN
91 RAISE EXCEPTION
92 'negotiated_price must be NULL for fixed-price tiers (contract_id: %)', NEW.contract_id;
93 END IF;
94
95 RETURN NEW;
96END;
97$$;
98
99CREATE OR REPLACE TRIGGER trg_vendor_subscription_pricing
100 BEFORE INSERT OR UPDATE ON Vendor_Subscription
101 FOR EACH ROW EXECUTE FUNCTION trg_enforce_negotiated_price();
102
103
104-- =============================================================================
105-- TRIGGER 6-12: Automatic updated_at Timestamp Maintenance
106-- =============================================================================
107CREATE OR REPLACE FUNCTION trg_set_updated_at()
108RETURNS TRIGGER LANGUAGE plpgsql AS $$
109BEGIN
110 NEW.updated_at := NOW();
111 RETURN NEW;
112END;
113$$;
114
115CREATE OR REPLACE TRIGGER trg_project_updated_at
116 BEFORE UPDATE ON Project
117 FOR EACH ROW EXECUTE FUNCTION trg_set_updated_at();
118
119CREATE OR REPLACE TRIGGER trg_vendor_subscription_updated_at
120 BEFORE UPDATE ON Vendor_Subscription
121 FOR EACH ROW EXECUTE FUNCTION trg_set_updated_at();
122
123CREATE OR REPLACE TRIGGER trg_cvc_updated_at
124 BEFORE UPDATE ON Client_Vendor_Contract
125 FOR EACH ROW EXECUTE FUNCTION trg_set_updated_at();
126
127CREATE OR REPLACE TRIGGER trg_pba_updated_at
128 BEFORE UPDATE ON Project_Budget_Audit
129 FOR EACH ROW EXECUTE FUNCTION trg_set_updated_at();
130
131CREATE OR REPLACE TRIGGER trg_review_updated_at
132 BEFORE UPDATE ON Review
133 FOR EACH ROW EXECUTE FUNCTION trg_set_updated_at();
134
135CREATE OR REPLACE TRIGGER trg_dispute_ticket_updated_at
136 BEFORE UPDATE ON Dispute_Ticket
137 FOR EACH ROW EXECUTE FUNCTION trg_set_updated_at();
138
139CREATE OR REPLACE TRIGGER trg_user_updated_at
140 BEFORE UPDATE ON "User"
141 FOR EACH ROW EXECUTE FUNCTION trg_set_updated_at();
142
143
144-- =============================================================================
145-- TRIGGER 13: Project Budget Change Audit
146-- =============================================================================
147CREATE OR REPLACE FUNCTION trg_capture_budget_change()
148RETURNS TRIGGER LANGUAGE plpgsql AS $$
149BEGIN
150 INSERT INTO Project_Budget_Audit (project_id, old_budget, new_budget)
151 VALUES (OLD.project_id, OLD.budget, NEW.budget);
152 RETURN NEW;
153END;
154$$;
155
156CREATE OR REPLACE TRIGGER trg_project_budget_audit
157 AFTER UPDATE ON Project
158 FOR EACH ROW
159 WHEN (OLD.budget IS DISTINCT FROM NEW.budget)
160 EXECUTE FUNCTION trg_capture_budget_change();
161
162
163-- =============================================================================
164-- TRIGGER 14: Project Status History Actor Validation
165-- =============================================================================
166CREATE OR REPLACE FUNCTION trg_validate_status_history_actor()
167RETURNS TRIGGER LANGUAGE plpgsql AS $$
168BEGIN
169 PERFORM fn_assert_project_exists(NEW.project_id);
170
171 -- A vendor actor must belong to the vendor on the project
172 IF NEW.vendor_user_id IS NOT NULL THEN
173 PERFORM fn_assert_vendor_user_exists(NEW.vendor_user_id);
174
175 IF NOT coalesce(fn_is_vendor_user_of_project(NEW.vendor_user_id, NEW.project_id), false) THEN
176 RAISE EXCEPTION
177 'vendor_user % does not belong to the vendor on project %',
178 NEW.vendor_user_id, NEW.project_id;
179 END IF;
180 END IF;
181
182 IF NEW.management_user_id IS NOT NULL THEN
183 PERFORM fn_assert_management_user_exists(NEW.management_user_id);
184 END IF;
185
186 RETURN NEW;
187END;
188$$;
189
190CREATE OR REPLACE TRIGGER trg_status_history_actor
191 BEFORE INSERT ON Project_Status_History
192 FOR EACH ROW EXECUTE FUNCTION trg_validate_status_history_actor();
193
194
195-- =============================================================================
196-- TRIGGER 15: Vendor Subscription Overlap Prevention
197-- =============================================================================
198-- Active and inactive periods alike are checked. end_date is exclusive, so a
199-- period may start on the day the previous one ends.
200-- contract_id <> NEW.contract_id excludes the row itself on UPDATE; on INSERT
201-- the SERIAL default is already assigned when the trigger runs.
202CREATE OR REPLACE FUNCTION trg_prevent_subscription_overlap()
203RETURNS TRIGGER LANGUAGE plpgsql AS $$
204BEGIN
205 IF EXISTS (
206 SELECT 1 FROM Vendor_Subscription
207 WHERE vendor_id = NEW.vendor_id
208 AND contract_id <> NEW.contract_id
209 AND (NEW.end_date IS NULL OR start_date < NEW.end_date)
210 AND (end_date IS NULL OR end_date > NEW.start_date)
211 ) THEN
212 RAISE EXCEPTION
213 'Vendor % already has a subscription period overlapping % - %',
214 NEW.vendor_id, NEW.start_date, coalesce(NEW.end_date::text, 'open');
215 END IF;
216
217 RETURN NEW;
218END;
219$$;
220
221CREATE OR REPLACE TRIGGER trg_vendor_subscription_overlap
222 BEFORE INSERT OR UPDATE ON Vendor_Subscription
223 FOR EACH ROW EXECUTE FUNCTION trg_prevent_subscription_overlap();
224
225
226-- =============================================================================
227-- TRIGGER 16: Dispute Ticket Resolution Validation
228-- =============================================================================
229CREATE OR REPLACE FUNCTION trg_validate_dispute_resolution()
230RETURNS TRIGGER LANGUAGE plpgsql AS $$
231BEGIN
232 IF NEW.is_resolved = true AND OLD.is_resolved = false THEN
233 IF NEW.assigned_management_user_id IS NULL THEN
234 RAISE EXCEPTION
235 'Dispute ticket % must have an assigned management user before it can be resolved', OLD.ticket_id;
236 END IF;
237
238 PERFORM fn_assert_management_user_exists(NEW.assigned_management_user_id);
239
240 IF NEW.resolved_at IS NULL THEN
241 NEW.resolved_at := NOW();
242 END IF;
243 END IF;
244
245 IF OLD.is_resolved = true AND NEW.is_resolved = false THEN
246 RAISE EXCEPTION 'Resolved dispute ticket % cannot be re-opened', OLD.ticket_id;
247 END IF;
248
249 RETURN NEW;
250END;
251$$;
252
253CREATE OR REPLACE TRIGGER trg_dispute_ticket_resolve
254 BEFORE UPDATE ON Dispute_Ticket
255 FOR EACH ROW EXECUTE FUNCTION trg_validate_dispute_resolution();
256
257
258-- =============================================================================
259-- TRIGGER 17: Disputed Review Publish Guard
260-- =============================================================================
261CREATE OR REPLACE FUNCTION trg_prevent_publishing_disputed_review()
262RETURNS TRIGGER LANGUAGE plpgsql AS $$
263BEGIN
264 IF NEW.is_published = true AND OLD.is_published = false THEN
265 IF EXISTS (
266 SELECT 1 FROM Dispute_Ticket
267 WHERE review_id = NEW.review_id
268 AND is_resolved = false
269 ) THEN
270 RAISE EXCEPTION
271 'Review % cannot be published while it has unresolved dispute tickets', NEW.review_id;
272 END IF;
273 END IF;
274
275 RETURN NEW;
276END;
277$$;
278
279CREATE OR REPLACE TRIGGER trg_review_publish_guard
280 BEFORE UPDATE ON Review
281 FOR EACH ROW EXECUTE FUNCTION trg_prevent_publishing_disputed_review();
282
283
284-- =============================================================================
285-- TRIGGER 18: Review Only for a Finished Project
286-- =============================================================================
287-- A review is the client's verdict on a finished engagement: the project must
288-- be Completed or Cancelled.
289CREATE OR REPLACE FUNCTION trg_require_finished_project()
290RETURNS TRIGGER LANGUAGE plpgsql AS $$
291DECLARE
292 v_status text;
293BEGIN
294 SELECT ps.status_name
295 INTO v_status
296 FROM Project p
297 JOIN Project_Status ps ON ps.status_id = p.status_id
298 WHERE p.project_id = NEW.project_id;
299
300 IF NOT FOUND THEN
301 RAISE EXCEPTION 'Project % does not exist', NEW.project_id;
302 END IF;
303
304 IF v_status NOT IN ('Completed', 'Cancelled') THEN
305 RAISE EXCEPTION 'Project % is %; a review needs a finished project', NEW.project_id, v_status;
306 END IF;
307
308 RETURN NEW;
309END;
310$$;
311
312CREATE OR REPLACE TRIGGER trg_review_finished_project
313 BEFORE INSERT OR UPDATE OF project_id ON Review
314 FOR EACH ROW EXECUTE FUNCTION trg_require_finished_project();