This repository was archived by the owner on Jan 23, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcreate.sql
More file actions
495 lines (424 loc) · 13.8 KB
/
create.sql
File metadata and controls
495 lines (424 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
-- ==========================================================
-- DATABASE CLEANUP
-- ==========================================================
DROP TABLE IF EXISTS
content_owner,
accepted_answer,
profile_view,
admin_create_moderator,
admin_create_admin,
admin_manage_reader,
admin_manage_tag,
reader_tag_follow,
reader_question_follow,
content_tag,
new_badge_notification,
new_vote_notification,
new_answer_notification,
notification,
reader_badge,
vote,
comment,
answer,
question,
content,
tag,
badge,
administrator,
moderator,
reader,
oauthapi
CASCADE;
DROP TYPE IF EXISTS VoteType CASCADE;
DROP TYPE IF EXISTS ActionType CASCADE;
-- ==========================================================
-- DOMAIN DEFINITIONS
-- ==========================================================
CREATE TYPE VoteType AS ENUM ('-1', '+1');
CREATE TYPE ActionType AS ENUM ('create', 'delete', 'block', 'unblock');
-- ==========================================================
-- TABLE DEFINITIONS
-- ==========================================================
-- R01: oauthapi
CREATE TABLE oauthapi (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
provider_name TEXT NOT NULL UNIQUE
);
-- R02: reader
CREATE TABLE reader (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
profile_picture TEXT,
registration_date DATE NOT NULL CHECK (registration_date <= CURRENT_DATE),
is_banned BOOLEAN NOT NULL DEFAULT FALSE,
id_oauth BIGINT REFERENCES oauthapi(id)
);
-- R03: content
CREATE TABLE content (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
creation_date DATE NOT NULL CHECK (creation_date <= CURRENT_DATE),
last_edited_date DATE NOT NULL CHECK (last_edited_date <= CURRENT_DATE AND last_edited_date >= creation_date),
content_body TEXT NOT NULL,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
id_author BIGINT NOT NULL REFERENCES reader(id),
id_last_editor BIGINT NOT NULL REFERENCES reader(id)
);
-- R04: question
CREATE TABLE question (
id BIGINT PRIMARY KEY REFERENCES content(id),
title TEXT NOT NULL
);
-- R05: answer
CREATE TABLE answer (
id BIGINT PRIMARY KEY REFERENCES content(id),
id_question BIGINT NOT NULL REFERENCES question(id)
);
-- R06: comment
CREATE TABLE comment (
id BIGINT PRIMARY KEY REFERENCES content(id),
parent_comment_id BIGINT REFERENCES comment(id),
parent_answer_id BIGINT REFERENCES answer(id),
CHECK (
(parent_comment_id IS NOT NULL AND parent_answer_id IS NULL) OR
(parent_comment_id IS NULL AND parent_answer_id IS NOT NULL)
)
);
-- R07: tag
CREATE TABLE tag (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
-- R08: vote
CREATE TABLE vote (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
value VoteType NOT NULL,
date DATE NOT NULL CHECK (date <= CURRENT_DATE),
id_voter BIGINT NOT NULL REFERENCES reader(id),
id_content BIGINT NOT NULL REFERENCES content(id)
);
-- R09: badge
CREATE TABLE badge (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL,
icon TEXT NOT NULL
);
-- R10: reader_badge
CREATE TABLE reader_badge (
id_reader BIGINT NOT NULL REFERENCES reader(id),
id_badge BIGINT NOT NULL REFERENCES badge(id),
PRIMARY KEY (id_reader, id_badge)
);
-- R11: notification
CREATE TABLE notification (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
date TIMESTAMP NOT NULL CHECK (date <= NOW()),
is_read BOOLEAN NOT NULL,
id_reader BIGINT NOT NULL REFERENCES reader(id)
);
-- R12: new_answer_notification
CREATE TABLE new_answer_notification (
id BIGINT PRIMARY KEY REFERENCES notification(id),
id_answer BIGINT NOT NULL REFERENCES answer(id)
);
-- R13: new_vote_notification
CREATE TABLE new_vote_notification (
id BIGINT PRIMARY KEY REFERENCES notification(id),
id_vote BIGINT NOT NULL REFERENCES vote(id)
);
-- R14: new_badge_notification
CREATE TABLE new_badge_notification (
id BIGINT PRIMARY KEY REFERENCES notification(id),
id_badge BIGINT NOT NULL REFERENCES badge(id)
);
-- R15: moderator
CREATE TABLE moderator (
id_reader BIGINT PRIMARY KEY REFERENCES reader(id),
moderator_since DATE NOT NULL CHECK (moderator_since <= CURRENT_DATE)
);
-- R16: administrator
CREATE TABLE administrator (
id_reader BIGINT PRIMARY KEY REFERENCES reader(id),
admin_since DATE NOT NULL CHECK (admin_since <= CURRENT_DATE)
);
-- R17: reader_question_follow
CREATE TABLE reader_question_follow (
id_reader BIGINT NOT NULL REFERENCES reader(id),
id_question BIGINT NOT NULL REFERENCES question(id),
PRIMARY KEY (id_reader, id_question)
);
-- R18: reader_tag_follow
CREATE TABLE reader_tag_follow (
id_reader BIGINT NOT NULL REFERENCES reader(id),
id_tag BIGINT NOT NULL REFERENCES tag(id),
PRIMARY KEY (id_reader, id_tag)
);
-- R19: content_tag
CREATE TABLE content_tag (
id_content BIGINT NOT NULL REFERENCES content(id),
id_tag BIGINT NOT NULL REFERENCES tag(id),
PRIMARY KEY (id_content, id_tag)
);
-- R20: admin_manage_reader
CREATE TABLE admin_manage_reader (
id_admin BIGINT NOT NULL REFERENCES administrator(id_reader),
id_reader BIGINT NOT NULL REFERENCES reader(id),
action_type ActionType NOT NULL,
action_date DATE NOT NULL CHECK (action_date <= CURRENT_DATE),
notes TEXT,
PRIMARY KEY (id_admin, id_reader, action_date)
);
-- R21: admin_manage_tag
CREATE TABLE admin_manage_tag (
id_admin BIGINT NOT NULL REFERENCES administrator(id_reader),
id_tag BIGINT NOT NULL REFERENCES tag(id),
action_type ActionType NOT NULL,
action_date DATE NOT NULL CHECK (action_date <= CURRENT_DATE),
PRIMARY KEY (id_admin, id_tag, action_date)
);
-- R22: accepted_answer
CREATE TABLE accepted_answer (
question_id BIGINT PRIMARY KEY REFERENCES question(id),
answer_id BIGINT UNIQUE REFERENCES answer(id)
);
-- R23: profile_view
CREATE TABLE profile_view (
viewer_id BIGINT NOT NULL REFERENCES reader(id),
viewed_id BIGINT NOT NULL REFERENCES reader(id),
view_date DATE NOT NULL,
PRIMARY KEY (viewer_id, viewed_id, view_date)
);
-- R24: admin_create_moderator
CREATE TABLE admin_create_moderator (
id_admin BIGINT NOT NULL REFERENCES administrator(id_reader),
id_moderator BIGINT NOT NULL REFERENCES moderator(id_reader),
creation_date DATE NOT NULL CHECK (creation_date <= CURRENT_DATE),
notes TEXT,
PRIMARY KEY (id_admin, id_moderator)
);
-- R25: admin_create_admin
CREATE TABLE admin_create_admin (
id_admin BIGINT NOT NULL REFERENCES administrator(id_reader),
id_new_admin BIGINT NOT NULL REFERENCES administrator(id_reader),
creation_date DATE NOT NULL CHECK (creation_date <= CURRENT_DATE),
notes TEXT,
PRIMARY KEY (id_admin, id_new_admin)
);
-- ==========================================================
-- INDEXES
-- ==========================================================
--IDX01
CREATE INDEX idx_answer_question ON answer(id_question);
--IDX02-A
CREATE INDEX idx_comment_parent_comment ON comment(parent_comment_id);
--IDX02-B
CREATE INDEX idx_comment_parent_answer ON comment(parent_answer_id);
--IDX03
CREATE INDEX idx_content_tag_tag ON content_tag(id_tag);
--IDX04
ALTER TABLE question
ADD COLUMN IF NOT EXISTS document_tsv tsvector;
CREATE OR REPLACE FUNCTION fn_update_question_tsv()
RETURNS trigger AS $$
DECLARE
v_content_body TEXT;
BEGIN
SELECT content_body INTO v_content_body
FROM content
WHERE id = NEW.id;
NEW.document_tsv :=
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(v_content_body, '')), 'B');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_update_question_tsv
BEFORE INSERT OR UPDATE ON question
FOR EACH ROW
EXECUTE FUNCTION fn_update_question_tsv();
CREATE OR REPLACE FUNCTION fn_refresh_question_tsv_from_content()
RETURNS trigger AS $$
BEGIN
IF EXISTS (SELECT 1 FROM question WHERE id = NEW.id) THEN
UPDATE question
SET document_tsv =
setweight(to_tsvector('english', title), 'A') ||
setweight(to_tsvector('english', NEW.content_body), 'B')
WHERE id = NEW.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_refresh_question_tsv_from_content ON content;
CREATE TRIGGER trg_refresh_question_tsv_from_content
AFTER UPDATE OF content_body ON content
FOR EACH ROW
EXECUTE FUNCTION fn_refresh_question_tsv_from_content();
CREATE INDEX IF NOT EXISTS idx_question_document_tsv
ON question USING GIN(document_tsv);
-- ==========================================================
-- TRIGGERS
-- ==========================================================
-- TRIGGER02: Validate accepted answer
CREATE OR REPLACE FUNCTION fn_check_accepted_answer()
RETURNS TRIGGER AS
$$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM answer
WHERE id = NEW.answer_id
AND id_question = NEW.question_id
) THEN
RAISE EXCEPTION
'Accepted answer (ID: %) does not belong to question (ID: %)',
NEW.answer_id, NEW.question_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_check_accepted_answer
BEFORE INSERT OR UPDATE ON accepted_answer
FOR EACH ROW
EXECUTE FUNCTION fn_check_accepted_answer();
-- TRIGGER03: Notify users on new answer
CREATE OR REPLACE FUNCTION fn_notify_new_answer()
RETURNS TRIGGER AS
$$
DECLARE
v_question_author_id BIGINT;
v_answer_author_id BIGINT;
v_follower RECORD;
v_notification_id BIGINT;
BEGIN
SELECT c.id_author INTO v_question_author_id
FROM question q
JOIN content c ON q.id = c.id
WHERE q.id = NEW.id_question;
SELECT id_author INTO v_answer_author_id
FROM content
WHERE id = NEW.id;
IF v_question_author_id IS NOT NULL AND v_question_author_id <> v_answer_author_id THEN
INSERT INTO notification(date, is_read, id_reader)
VALUES (NOW(), FALSE, v_question_author_id)
RETURNING id INTO v_notification_id;
INSERT INTO new_answer_notification(id, id_answer)
VALUES (v_notification_id, NEW.id);
END IF;
FOR v_follower IN
SELECT id_reader FROM reader_question_follow
WHERE id_question = NEW.id_question
AND id_reader <> v_answer_author_id
LOOP
INSERT INTO notification(date, is_read, id_reader)
VALUES (NOW(), FALSE, v_follower.id_reader)
RETURNING id INTO v_notification_id;
INSERT INTO new_answer_notification(id, id_answer)
VALUES (v_notification_id, NEW.id);
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER trg_notify_new_answer
AFTER INSERT ON answer
FOR EACH ROW
EXECUTE FUNCTION fn_notify_new_answer();
-- TRIGGER04: Notify content author on new vote
CREATE OR REPLACE FUNCTION fn_notify_new_vote()
RETURNS TRIGGER AS
$$
DECLARE
v_content_author_id BIGINT;
v_notification_id BIGINT;
BEGIN
SELECT id_author INTO v_content_author_id
FROM content
WHERE id = NEW.id_content;
IF v_content_author_id IS NOT NULL AND v_content_author_id <> NEW.id_voter THEN
INSERT INTO notification (date, is_read, id_reader)
VALUES (NOW(), FALSE, v_content_author_id)
RETURNING id INTO v_notification_id;
INSERT INTO new_vote_notification (id, id_vote)
VALUES (v_notification_id, NEW.id);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_notify_new_vote
AFTER INSERT ON vote
FOR EACH ROW
EXECUTE FUNCTION fn_notify_new_vote();
-- TRIGGER05: Notify reader on new badge
CREATE OR REPLACE FUNCTION fn_notify_new_badge()
RETURNS TRIGGER AS
$$
DECLARE
v_notification_id BIGINT;
BEGIN
INSERT INTO notification (date, is_read, id_reader)
VALUES (CURRENT_DATE, FALSE, NEW.id_reader)
RETURNING id INTO v_notification_id;
INSERT INTO new_badge_notification (id, id_badge)
VALUES (v_notification_id, NEW.id_badge);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_notify_new_badge
AFTER INSERT ON reader_badge
FOR EACH ROW
EXECUTE FUNCTION fn_notify_new_badge();
-- TRIGGER07: Remove moderator/admin roles when reader is deleted or banned
CREATE OR REPLACE FUNCTION fn_cleanup_roles_on_reader_change()
RETURNS TRIGGER AS
$$
BEGIN
IF TG_OP = 'DELETE'
OR (TG_OP = 'UPDATE' AND NEW.is_banned = TRUE AND OLD.is_banned = FALSE) THEN
DELETE FROM moderator WHERE id_reader = OLD.id;
DELETE FROM administrator WHERE id_reader = OLD.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_cleanup_roles_on_reader_change
AFTER UPDATE OR DELETE ON reader
FOR EACH ROW
EXECUTE FUNCTION fn_cleanup_roles_on_reader_change();
-- TRIGGER08: Auto-update content.last_edited_date on content changes
CREATE OR REPLACE FUNCTION fn_auto_update_last_edited_date()
RETURNS TRIGGER AS
$$
BEGIN
IF NEW.content_body IS DISTINCT FROM OLD.content_body THEN
NEW.last_edited_date := CURRENT_DATE;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_auto_update_last_edited_date
BEFORE UPDATE ON content
FOR EACH ROW
EXECUTE FUNCTION fn_auto_update_last_edited_date();
-- TRIGGER09: Clean up follower references when questions or tags are deleted
CREATE OR REPLACE FUNCTION fn_cleanup_follows()
RETURNS TRIGGER AS
$$
BEGIN
IF TG_TABLE_NAME = 'question' THEN
DELETE FROM reader_question_follow WHERE id_question = OLD.id;
ELSIF TG_TABLE_NAME = 'tag' THEN
DELETE FROM reader_tag_follow WHERE id_tag = OLD.id;
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_cleanup_question_follows
AFTER DELETE ON question
FOR EACH ROW
EXECUTE FUNCTION fn_cleanup_follows();
CREATE TRIGGER trg_cleanup_tag_follows
AFTER DELETE ON tag
FOR EACH ROW
EXECUTE FUNCTION fn_cleanup_follows();