-
Notifications
You must be signed in to change notification settings - Fork 0
ebd
This section is dedicated to the database design and implementation process, which involves translating the UML specifications into a relational schema and subsequently implementing it in PostgreSQL.
In this subsection, we'll explore the conceptual data model, a vital step in our database design process. We use UML specifications to define data elements and relationships, creating a high-level data overview. This model guides the construction of our PostgreSQL relational schema, helping us understand key data modeling concepts and principles for this report.
This section serves as the bridge between the conceptual model represented by UML and the practical implementation of the database in SQL. The relational schema acts as an intermediary abstraction layer, facilitating the transition from the conceptual representation to the concrete database structure in SQL.
Name | Definition |
---|---|
R01 | user(id,username UK NN, password NN, image, email UK NN, academic_status, display_name, is_private NN DF true, role, email_verified_at) |
R02 | post(id, author |
R03 | friend(id_friend_1 |
R04 | group(id, name NN, description NN, is_private NN DF true) |
R05 | group_owner(user_id |
R06 | group_request(id, user_id |
R07 | group_user(user_id |
R08 | friend_request(id, user_id |
R09 | comment(id, post_id |
R10 | reaction(id, date, post_id |
R11 | post_tag(id,post_id |
R12 | post_tag_not(id, post_tag_id |
R13 | group_request_not(id, group_req_id |
R14 | friend_request_not(id, friend_req_id |
R15 | comment_not(id, comment |
R16 | reaction_not(id, reaction_id |
R17 | group_ban(id, group |
R18 | app_ban(id, banned |
R19 | appeal(id, reason) |
R20 | password_reset_tokens(email, token, created_at) |
We've decided to use user roles within the users relation due to the lack of completeness for child entities. In contrast, for the notifications and bans tables, which are complete and have distinct attributes for each child entity, we've created separate relations for each child while inheriting common attributes from the parent.
Name | Definition |
---|---|
Today | DATE DEFAULT CURRENT_DATE |
ReactionType | ENUM ('LIKE', 'DISLIKE', 'HEART', 'STAR'); |
Table R01 (users) | |
---|---|
Keys | {id, username, email} |
Functional Dependencies | |
FD0101 | {id} |
FD0102 | {username} |
FD0103 | {email} |
Normal Form | BCNF |
Table R02 (post) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD0201 | {id} |
Normal Form | BCNF |
Table R03 (friend) | |
---|---|
Keys | {id_friend_1, id_friend_2} |
Functional Dependencies | none |
Normal Form | BCNF |
Table R04 (group) | |
---|---|
Keys | {id, name} |
Functional Dependencies | |
FD0401 | {id} |
FD0402 | {name} |
Normal Form | BCNF |
Table R05 (group_owner) | |
---|---|
Keys | {user_id, group_id} |
Functional Dependencies | none |
Normal Form | BCNF |
Table R06 (group_request) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD0601 | {id} |
Normal Form | BCNF |
Table R07 (group_user) | |
---|---|
Keys | {user_id,group_id} |
Functional Dependencies | none |
Normal Form | BCNF |
Table R08 (friend_request) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD0801 | {id} |
Normal Form | BCNF |
Table R09 (comment) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD0901 | {id} |
Normal Form | BCNF |
Table R10 (reaction) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1001 | {id} |
Normal Form | BCNF |
Table R11 (post_tag) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1101 | {id} |
Normal Form | BCNF |
Table R12 (post_tag_not) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1201 | {id} |
Normal Form | BCNF |
Table R13 (group_request_not) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1301 | {id} |
Normal Form | BCNF |
Table R14 (friend_request_not) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1401 | {id} |
Normal Form | BCNF |
Table R15 (comment_not) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1501 | {id} |
Normal Form | BCNF |
Table R16 (reaction_not) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1601 | {id} |
Normal Form | BCNF |
Table R17 (group_ban) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1701 | {id} |
Normal Form | BCNF |
Table R18 (app_ban) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1801 | {id} |
Normal Form | BCNF |
Table R19 (appeal) | |
---|---|
Keys | {id} |
Functional Dependencies | |
FD1901 | {id} |
Normal Form | BCNF |
Table R20 (password_reset_tokens) | |
---|---|
Keys | {email} |
Functional Dependencies | |
FD2001 | {email} \rightarrow {token, created_at} |
Normal Form | BCNF |
Since all the attributes in each non-trivial relation can be accessed through a superkey, we can say that these relations are in Boyce-Codd Normal Form (BCNF).
Relation | Relation name | Order of magnitude | Estimated growth |
---|---|---|---|
R01 | user | 1k | 10 / day |
R02 | post | 100k | 1k / day |
R03 | friend | 100 | 10 / day |
R04 | group | 100 | 1 / day |
R05 | group_owner | 100 | 1 / day |
R06 | group_request | 1k | 10 / day |
R07 | group_user | 10k | 10 / day |
R08 | friend_request | 1k | 10 / day |
R09 | comment | 100k | 1k / day |
R10 | reaction | 100k | 1k / day |
R11 | post_tag | 10k | 1k / day |
R12 | post_tag_not | 10k | 1k / day |
R13 | group_request_not | 1k | 10 / day |
R14 | friend_request_not | 1k | 10 / day |
R15 | comment_not | 100k | 1k / day |
R16 | reaction_not | 100k | 1k / day |
R17 | group_ban | 100 | 1 / day |
R18 | app_ban | 100 | 1 / day |
R19 | appeal | 100 | 1 / day |
R20 | password_reset_tokens | 500 | 10/day |
Index | IDX01 |
Index Relation | users |
Index attribute | id |
Index type | Hash |
Cardinality | High |
Clustering | None |
Justification | The majority of searches on this table will use the id of the user as the parameter to select the respective row, and because we only want the attributes of one row, which at average will be an O(1) operation on an hash set, we selected that data structure instead of the BTrees. |
SQL Code | CREATE INDEX user_index ON users USING hash(id) |
Index | IDX02 |
Index Relation | posts |
Index attribute | author |
Index type | B-Tree |
Cardinality | High |
Clustering | No |
Justification | We decided against clustering because the post table is a table with frequent inserts since posts are expected to be added daily. In addition, we chose to go with a B-tree because an author has many posts, and doing a hash table per author would not be feasible. |
SQL Code | CREATE INDEX author_post ON post USING btree(author) |
Index | IDX03 |
Index Relation | comment |
Index attribute | post_id |
Index type | B-Tree |
Cardinality | Medium |
Clustering | No |
Justification | There is no clustering because the frequency of new comments is expected to be fairly high. |
SQL Code | CREATE INDEX post_comment ON comment USING btree(post_id) |
Index | IDX04 |
---|---|
Relation | users |
Attributes | username, display_name |
Type | GIN |
Clustering | No |
Justification | The usernames and display names of the users will not be updated often, so we chose to go with a GIN index. |
ALTER TABLE users ADD COLUMN tsvectors TSVECTOR;
CREATE FUNCTION update_users_search() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = setweight(to_tsvector('english', NEW.username), 'A')
|| setweight(to_tsvector('english', NEW.display_name), 'B');
END IF;
IF TG_OP = 'UPDATE' THEN
IF (NEW.username <> OLD.username) THEN
NEW.tsvectors = setweight(to_tsvector('english', NEW.username), 'A')
|| setweight(to_tsvector('english', NEW.display_name), 'B');
END IF;
END IF;
END;
CREATE TRIGGER update_users_search
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW
EXECUTE PROCEDURE update_users_search();
CREATE_INDEX user_search_idx ON users USING GIN (tsvectors);
Index | IDX05 |
---|---|
Relation | post |
Attributes | title, content |
Type | GIN |
Clustering | No |
Justification | Full text search for posts based on the title of the post, its content and its author. As posts are not updated often, we chose to use GIN index, which has faster lookups. |
ALTER TABLE post ADD COLUMN tsvectors TSVECTOR;
CREATE FUNCTION update_post_search() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = setweight(to_tsvector('english', NEW.title), 'A') ||
setweight(to_tsvector('english', NEW.content), 'B') ||
setweight(to_tsvector('english', NEW.username), 'C');
END IF;
IF TG_OP = 'UPDATE' THEN
IF (NEW.title <> OLD.title OR NEW.content <> OLD.content OR NEW.username <> OLD.username) THEN
NEW.tsvectors = setweight(to_tsvector('english', NEW.title), 'A') ||
setweight(to_tsvector('english', NEW.content), 'B') ||
setweight(to_tsvector('english', NEW.username), 'C');
END IF;
END IF;
END;
CREATE TRIGGER update_post_search
BEFORE INSERT OR UPDATE ON post
FOR EACH ROW
EXECUTE PROCEDURE update_post_search();
CREATE_INDEX post_search_idx ON post USING GIN(tsvectors);
Trigger | TRIGGER01 |
---|---|
Description | Alter the user id to the default one when a user is deleted |
CREATE OR REPLACE FUNCTION update_deleted_user() RETURNS TRIGGER AS
$BODY$
BEGIN
DELETE FROM post_tag_not WHERE id = (
SELECT post_tag.id
FROM post_tag_not JOIN post_tag ON post_tag_not.post_id = post_tag.id
JOIN users ON users.id = post_tag.user_id
WHERE users.id = OLD.id
);
DELETE FROM post_tag_not WHERE id = (
SELECT post_tag.id
FROM post_tag_not JOIN post_tag ON post_tag_not.post_id = post_tag.id
JOIN users ON users.id = post_tag.user_id
WHERE users.id = OLD.id
);
DELETE FROM group_request_not WHERE id = (
SELECT group_request.id
FROM group_request_not JOIN group_request ON group_request_not.group_req_id = group_request.id
JOIN users ON users.id = group_request.user_id
WHERE users.id = OLD.id
);
DELETE FROM friend_request_not WHERE id = (
SELECT friend_request.id
FROM friend_request_not JOIN friend_request ON friend_request_not.friend_req_id = friend_request.id
JOIN users ON users.id = friend_request.user_id OR users.id = friend_request.friend_id
WHERE users.id = OLD.id
);
DELETE FROM comment_not WHERE comment = (
SELECT comment.id
FROM comment_not JOIN comment ON comment_not.comment = comment.id
JOIN users ON users.id = comment.author
WHERE users.id = OLD.id
);
DELETE FROM reaction_not WHERE reaction_id = (
SELECT reaction.id
FROM reaction_not JOIN reaction ON reaction_not.reaction_id = reaction.id
JOIN users ON users.id = reaction.author_id
WHERE users.id = OLD.id
);
UPDATE post SET author = 0 WHERE author = OLD.id;
UPDATE comment SET author = 0 WHERE user_id = OLD.id;
UPDATE reaction SET author = 0 WHERE user_id = OLD.id;
DELETE FROM group_owner WHERE user_id = OLD.id;
RETURN OLD;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_deleted_user_trigger
AFTER DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION update_deleted_user();
Trigger | TRIGGER02 |
---|---|
Description | When a comment is added, a notification will be inserted |
CREATE OR REPLACE FUNCTION update_comment_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO comment_not (comment_id, date)
VALUES (NEW.id, now());
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_comment_not_trigger
AFTER INSERT ON comment
FOR EACH ROW
EXECUTE FUNCTION update_comment_not();
Trigger | TRIGGER03 |
---|---|
Description | When a user is tagged in a post, a notification will be inserted |
CREATE OR REPLACE FUNCTION update_tag_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT ON post_tag_not (post_tag_id, date)
VALUES (NEW.id, now());
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_tag_not_trigger
AFTER INSERT ON post_tag
FOR EACH ROW
EXECUTE FUNCTION update_comment_not();
Trigger | TRIGGER04 |
---|---|
Description | When a reaction is added, a notification will be inserted |
CREATE OR REPLACE FUNCTION update_reaction_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO reaction_not (reaction_id, date)
VALUES (NEW.id, now());
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_reaction_not_trigger
AFTER INSERT ON reaction
FOR EACH ROW
EXECUTE FUNCTION update_reaction_not();
Trigger | TRIGGER05 |
---|---|
Description | When a group request is added, a notification will be inserted |
CREATE OR REPLACE FUNCTION update_group_request_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO group_request_not (group_request_id, date)
VALUES (NEW.id, now());
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_group_request_not_trigger
AFTER INSERT ON group_request
FOR EACH ROW
EXECUTE FUNCTION update_group_request_not();
Trigger | TRIGGER06 |
---|---|
Description | When a friend request is added, a notification will be inserted |
CREATE OR REPLACE FUNCTION update_friend_request_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO friend_request_not (friend_request, date)
VALUES (NEW.id, now());
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_friend_request_not_trigger
AFTER INSERT ON friend_request
FOR EACH ROW
EXECUTE FUNCTION update_friend_request_not();
Trigger | TRIGGER07 |
---|---|
Description | A user can only add posts to groups which he belongs |
CREATE OR REPLACE FUNCTION check_belongs_group() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (NOT EXISTS (SELECT * FROM group_user where group_user.user_id = NEW.author and group_user.group_id = NEW.group_id)
AND (NEW.group_id <> null)) THEN
RAISE EXCEPTION 'The user must belong to the group to add a post';
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_belongs_group_trigger
BEFORE INSERT ON post
FOR EACH ROW
EXECUTE FUNCTION check_belongs_group();
Trigger | TRIGGER08 |
---|---|
Description | A user cannot send a friend request to himself |
CREATE OR REPLACE FUNCTION check_friend_himself() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (NEW.user_id = NEW.friend_id) THEN
RAISE EXCEPTION 'A user cannot send friend request to himself.';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_friend_of_himself
BEFORE INSERT ON friend_request
EXECUTE FUNCTION check_friend_himself();
Trigger | TRIGGER09 |
---|---|
Description | A friend request must only be sent to non-friends. |
CREATE OR REPLACE FUNCTION check_friendship_exists() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (SELECT * FROM friends where (friend1 = NEW.user_id and friend2 = NEW.friend_id) or
(friend2 = NEW.user_id and friend1 = NEW.friend_id)) THEN
RAISE EXCEPTION 'The users are already friends';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_friendship_exists
BEFORE INSERT ON friend_request
EXECUTE FUNCTION check_friendship_exists();
Trigger | TRIGGER10 |
---|---|
Description | A group request must only be sent to non-members of group. |
CREATE OR REPLACE FUNCTION check_group_request() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (SELECT * FROM group_user where user_id = NEW.user_id and group_id = NEW.group_id) THEN
RAISE EXCEPTION 'The user is already a member of the group';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_group_request
BEFORE INSERT ON group_request
EXECUTE FUNCTION check_group_request();
Trigger | TRIGGER11 |
---|---|
Description | When a group request is accepted, the user is added to the group. |
CREATE OR REPLACE FUNCTION add_user_to_group() RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.is_accepted = true THEN
INSERT INTO group_user (user_id, group_id) VALUES (NEW.user_id, NEW.group_id);
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER add_user_to_group
AFTER INSERT OR UPDATE ON group_request
FOR EACH ROW
WHEN (NEW.is_accepted = true)
EXECUTE FUNCTION add_user_to_group();
Trigger | TRIGGER12 |
---|---|
Description | When a friend request is accepted, the users are now friends. |
CREATE OR REPLACE FUNCTION add_friend() RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.is_accepted = true THEN
INSERT INTO friends (friend1, friend2) VALUES (NEW.user_id, NEW.friend_id);
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER add_friend
AFTER INSERT OR UPDATE ON friend_request
FOR EACH ROW
WHEN (NEW.is_accepted = true)
EXECUTE FUNCTION add_friend();
Transaction | TRAN01 |
---|---|
Description | When a User is deleted, his posts must become anonymous |
Justification | Due to the fact that a user deletes his account, his data must be kept accessible for the other users. However, this posts must be anonymized |
Isolation Level | SERIALIZABLE READ ONLY, to ensure that the data accessed during the transaction remains stable and consistent |
BEGIN TRANSACTION;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY;
UPDATE post
SET author = 0
WHERE author = OLD.id;
UPDATE comment
SET author = 0
WHERE user_id = OLD.id;
UPDATE reaction
SET author = 0
WHERE user_id = OLD.id;
DELETE
FROM group_owner
WHERE user_id = OLD.id;
DELETE
FROM post_tag_not ptn
JOIN post p ON(ptn.post_id = p.id)
JOIN users u ON (p.author = u.id)
WHERE u.id = OLD.id;
DELETE
FROM group_request_not grn
JOIN group_request gr ON(grn.group_request_id = gr.id)
JOIN users u ON (g.user_id = u.id)
WHERE u.id = OLD.id;
DELETE
FROM friend_request_not frn
JOIN friend_request fr ON(frn.friend_request = fr.id)
JOIN users u ON (fr.user_id = u.id OR fr.friend_id = u.id)
WHERE u.id = OLD.id;
DELETE
FROM comment_not cn
JOIN comment c ON(cn.comment_id = c.id)
JOIN users u ON (c.author = u.id)
WHERE u.id = OLD.id;
DELETE
FROM reaction_not rn
JOIN reaction r ON(rn.reaction_id = r.id)
JOIN users u ON (r.author = u.id)
WHERE u.id = OLD.id;
DELETE
FROM group_user
WHERE user_id = OLD.id;
END TRANSACTION;
Changes made to the first submission:
- Added password_reset_tokens to the relational schema and the schema validation (23th november 2023)
GROUP2391, 23/10/2023
- Davide Teixeira, up202109860@up.pt
- João Fernandes, up202108867@up.pt
- Pedro Oliveira, up202108669@up.pt
- Tomás Palma, up202108880@up.pt
Editors
- Davide Teixeira, up202109860@up.pt
- João Fernandes, up202108867@up.pt
- Pedro Oliveira, up202108669@up.pt
- Tomás Palma, up202108880@up.pt
CREATE SCHEMA IF not exists lbaw2391;
DROP TABLE IF exists users CASCADE;
DROP TABLE IF exists post CASCADE;
DROP TABLE IF exists groups CASCADE;
DROP TABLE IF exists group_owner CASCADE;
DROP TABLE IF exists group_request CASCADE;
DROP TABLE IF exists group_user CASCADE;
DROP TABLE IF exists friend_request CASCADE;
DROP TABLE IF exists comment CASCADE;
DROP TABLE IF exists reaction CASCADE;
DROP TABLE IF exists post_tag_not CASCADE;
DROP TABLE IF exists group_request_not CASCADE;
DROP TABLE IF exists friend_request_not CASCADE;
DROP TABLE IF exists post_tag CASCADE;
DROP TABLE IF exists comment_not CASCADE;
DROP TABLE IF exists reaction_not CASCADE;
DROP TABLE IF exists group_ban CASCADE;
DROP TABLE IF exists app_ban CASCADE;
DROP TABLE IF exists friends CASCADE;
DROP TABLE IF exists appeal CASCADE;
-----------------------------------------
-- Types
-----------------------------------------
DROP TYPE if exists reaction_types;
CREATE TYPE reaction_types AS ENUM ('LIKE', 'DISLIKE', 'HEART', 'STAR');
-----------------------------------------
-- Tables
-----------------------------------------
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL CONSTRAINT unique_username UNIQUE,
email TEXT NOT NULL CONSTRAINT unique_email UNIQUE,
password TEXT NOT NULL,
image TEXT,
academic_status TEXT,
display_name TEXT,
is_private BOOLEAN DEFAULT true NOT NULL,
role INTEGER NOT NULL
);
CREATE TABLE groups (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL CONSTRAINT unique_group_name UNIQUE,
is_private BOOLEAN DEFAULT true NOT NULL,
description TEXT NOT NULL
);
CREATE TABLE post (
id SERIAL PRIMARY KEY,
author INTEGER REFERENCES users(id),
title TEXT NOT NULL,
content TEXT NOT NULL,
attachment TEXT,
group_id INTEGER REFERENCES groups(id),
is_private BOOLEAN NOT NULL,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK (date <= now())
);
CREATE TABLE friends (
friend1 INTEGER REFERENCES users(id),
friend2 INTEGER REFERENCES users(id),
PRIMARY KEY (friend1, friend2)
);
CREATE TABLE group_owner(
user_id INTEGER REFERENCES users(id) ON UPDATE CASCADE,
group_id INTEGER REFERENCES groups(id) ON UPDATE CASCADE,
PRIMARY KEY (user_id, group_id)
);
CREATE TABLE group_request(
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON UPDATE CASCADE,
group_id INTEGER REFERENCES groups(id) ON UPDATE CASCADE,
is_accepted BOOLEAN DEFAULT false NOT NULL,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK (date <= now())
);
CREATE TABLE group_user(
user_id INTEGER REFERENCES users(id) ON UPDATE CASCADE,
group_id INTEGER REFERENCES groups(id) ON UPDATE CASCADE,
PRIMARY KEY (user_id, group_id)
);
CREATE TABLE friend_request(
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON UPDATE CASCADE,
friend_id INTEGER REFERENCES users(id) ON UPDATE CASCADE,
is_accepted BOOLEAN DEFAULT false,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK (date <= now())
);
CREATE TABLE comment(
id SERIAL PRIMARY KEY,
post_id INTEGER REFERENCES post(id) ON UPDATE CASCADE,
author INTEGER REFERENCES users(id) ON UPDATE CASCADE,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK (date <= now()),
content TEXT NOT NULL
);
CREATE TABLE reaction (
id SERIAL PRIMARY KEY,
post_id INTEGER REFERENCES post(id) ON UPDATE CASCADE,
comment_id INTEGER REFERENCES comment(id) ON UPDATE CASCADE,
author INTEGER REFERENCES users(id),
type reaction_types NOT NULL,
CONSTRAINT valid_post_and_comment_ck CHECK((post_id IS NULL and comment_id IS NOT NULL) or (post_id IS NOT NULL and comment_id IS NULL))
);
CREATE TABLE post_tag_not(
id SERIAL PRIMARY KEY,
post_id INTEGER REFERENCES post(id) ON UPDATE CASCADE,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK (date <= now())
);
CREATE TABLE post_tag(
id SERIAL PRIMARY KEY,
post_id INTEGER REFERENCES post(id) ON UPDATE CASCADE,
user_id INTEGER REFERENCES users(id) ON UPDATE CASCADE
);
CREATE TABLE group_request_not(
id SERIAL PRIMARY KEY,
group_request_id INTEGER REFERENCES group_request(id) ON UPDATE CASCADE,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK(date <= now())
);
CREATE TABLE friend_request_not(
id SERIAL PRIMARY KEY,
friend_request INTEGER REFERENCES friend_request(id) ON UPDATE CASCADE,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK(date <= now())
);
CREATE TABLE comment_not(
id SERIAL PRIMARY KEY,
comment_id INTEGER REFERENCES comment(id) ON UPDATE CASCADE,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK(date <= now())
);
CREATE TABLE reaction_not(
id SERIAL PRIMARY KEY,
reaction_id INTEGER REFERENCES reaction(id) ON UPDATE CASCADE,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK(date <= now())
);
CREATE TABLE appeal(
id SERIAL PRIMARY KEY,
reason TEXT NOT NULL
);
CREATE TABLE group_ban(
id SERIAL PRIMARY KEY,
reason TEXT NOT NULL,
group_owner_id INTEGER REFERENCES users(id) ON UPDATE CASCADE,
banned_user_id INTEGER REFERENCES users(id) ON UPDATE CASCADE,
group_id INTEGER REFERENCES groups(id) ON UPDATE CASCADE,
appeal INTEGER REFERENCES appeal(id) ON UPDATE CASCADE,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK(date <= now())
);
CREATE TABLE app_ban(
id SERIAL PRIMARY KEY,
reason TEXT NOT NULL,
admin_id INTEGER REFERENCES users(id) ON UPDATE CASCADE,
banned_user_id INTEGER REFERENCES users(id) ON UPDATE CASCADE,
appeal INTEGER REFERENCES appeal(id) ON UPDATE CASCADE,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL CHECK(date <= now())
);
CREATE INDEX user_index ON users USING hash(id);
CREATE INDEX post_comment ON comment USING btree(post_id);
CREATE INDEX author_post ON post USING btree(author);
-----------------------------------------
-- Full-text search
-----------------------------------------
-- Search users by username
ALTER TABLE users ADD COLUMN tsvectors TSVECTOR;
CREATE OR REPLACE FUNCTION update_users_search() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = to_tsvector('english', NEW.username);
END IF;
IF TG_OP = 'UPDATE' THEN
IF (NEW.username <> OLD.username) THEN
NEW.tsvectors = to_tsvector('english', NEW.username);
END IF;
END IF;
RETURN NEW;
END $$
LANGUAGE plpgsql;
CREATE TRIGGER update_users_search
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW
EXECUTE PROCEDURE update_users_search();
CREATE INDEX user_search_idx ON users USING GIN (tsvectors);
-----------------------------------------
-- Search posts
ALTER TABLE post ADD COLUMN tsvectors TSVECTOR;
CREATE OR REPLACE FUNCTION update_post_search() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = setweight(to_tsvector('english', NEW.title), 'A')
|| setweight(to_tsvector('english', NEW.content), 'B');
END IF;
IF TG_OP = 'UPDATE' THEN
IF (OLD.title <> NEW.title OR NEW.content <> OLD.content) THEN
NEW.tsvectors = setweight(to_tsvector('english', NEW.title), 'A')
|| setweight(to_tsvector('english', NEW.content), 'B');
END IF;
END IF;
RETURN NEW;
END$$
LANGUAGE plpgsql;
CREATE TRIGGER update_post_search
BEFORE INSERT OR UPDATE ON post
FOR EACH ROW
EXECUTE PROCEDURE update_post_search();
CREATE INDEX post_search_idx ON post USING GIN(tsvectors);
-- Search groups
ALTER TABLE groups ADD COLUMN tsvectors TSVECTOR;
CREATE OR REPLACE FUNCTION update_groups_search() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = setweight(to_tsvector('english', NEW.name), 'A') ||
setweight(to_tsvector('english', NEW.description), 'B');
END IF;
IF TG_OP = 'UPDATE' THEN
IF (NEW.name <> OLD.name OR NEW.description <> OLD.description) THEN
NEW.tsvectors = setweight(to_tsvector('english', NEW.name), 'A') ||
setweight(to_tsvector('english', NEW.description), 'B');
END IF;
END IF;
RETURN NEW;
END$$
LANGUAGE plpgsql;
CREATE TRIGGER update_groups_search
BEFORE INSERT OR UPDATE ON groups
FOR EACH ROW
EXECUTE PROCEDURE update_groups_search();
CREATE INDEX groups_search_idx ON groups USING GIN(tsvectors);
-----------------------------------------
-- Triggers
-----------------------------------------
-- (TRIGGER01) If a user is deleted, it will change all his activity to anonymous
CREATE OR REPLACE FUNCTION update_deleted_user() RETURNS TRIGGER AS
$BODY$
BEGIN
DELETE FROM post_tag_not WHERE id = (
SELECT post_tag.id
FROM post_tag_not JOIN post_tag ON post_tag_not.post_id = post_tag.id
JOIN users ON users.id = post_tag.user_id
WHERE users.id = OLD.id
);
DELETE FROM post_tag_not WHERE id = (
SELECT post_tag.id
FROM post_tag_not JOIN post_tag ON post_tag_not.post_id = post_tag.id
JOIN users ON users.id = post_tag.user_id
WHERE users.id = OLD.id
);
DELETE FROM group_request_not WHERE id = (
SELECT group_request.id
FROM group_request_not JOIN group_request ON group_request_not.group_req_id = group_request.id
JOIN users ON users.id = group_request.user_id
WHERE users.id = OLD.id
);
DELETE FROM friend_request_not WHERE id = (
SELECT friend_request.id
FROM friend_request_not JOIN friend_request ON friend_request_not.friend_req_id = friend_request.id
JOIN users ON users.id = friend_request.user_id OR users.id = friend_request.friend_id
WHERE users.id = OLD.id
);
DELETE FROM comment_not WHERE comment = (
SELECT comment.id
FROM comment_not JOIN comment ON comment_not.comment = comment.id
JOIN users ON users.id = comment.author
WHERE users.id = OLD.id
);
DELETE FROM reaction_not WHERE reaction_id = (
SELECT reaction.id
FROM reaction_not JOIN reaction ON reaction_not.reaction_id = reaction.id
JOIN users ON users.id = reaction.author_id
WHERE users.id = OLD.id
);
UPDATE post SET author = 0 WHERE author = OLD.id;
UPDATE comment SET author = 0 WHERE user_id = OLD.id;
UPDATE reaction SET author = 0 WHERE user_id = OLD.id;
DELETE FROM group_owner WHERE user_id = OLD.id;
RETURN OLD;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_deleted_user_trigger
AFTER DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION update_deleted_user();
-----------------------------------------
-- (TRIGGER02) Insert a notification when a comment is made in owner post
CREATE OR REPLACE FUNCTION update_comment_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO comment_not (comment_id, date)
VALUES (NEW.id, now());
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_comment_not_trigger
AFTER INSERT ON comment
FOR EACH ROW
EXECUTE FUNCTION update_comment_not();
-----------------------------------------
-- (TRIGGER03) When a user is tagged, a tagged notification is created
CREATE OR REPLACE FUNCTION update_tag_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO post_tag_not (post_tag_id, date)
VALUES (NEW.id, now());
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_tag_not_trigger
AFTER INSERT ON post_tag
FOR EACH ROW
EXECUTE FUNCTION update_comment_not();
-----------------------------------------
-- (TRIGGER04) When a reaction is made, a reaction notification is created
CREATE OR REPLACE FUNCTION update_reaction_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO reaction_not (reaction_id, date)
VALUES (NEW.id, now());
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_reaction_not_trigger
AFTER INSERT ON reaction
FOR EACH ROW
EXECUTE FUNCTION update_reaction_not();
-----------------------------------------
-- (TRIGGER05) When a user receives a group request, a notification is created
CREATE OR REPLACE FUNCTION update_group_request_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO group_request_not (group_request_id, date)
VALUES (NEW.id, now());
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_group_request_not_trigger
AFTER INSERT ON group_request
FOR EACH ROW
EXECUTE FUNCTION update_group_request_not();
-----------------------------------------
-- (TRIGGER06) When a friend request is added, a notification will be inserted
CREATE OR REPLACE FUNCTION update_friend_request_not() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO friend_request_not (friend_request, date)
VALUES (NEW.id, now());
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_friend_request_not_trigger
AFTER INSERT ON friend_request
FOR EACH ROW
EXECUTE FUNCTION update_friend_request_not();
-----------------------------------------
-- (TRIGGER07) A user can only add posts to groups which he belongs
CREATE OR REPLACE FUNCTION check_belongs_group() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (NOT EXISTS (SELECT * FROM group_user where group_user.user_id = NEW.author and group_user.group_id = NEW.group_id)
AND (NEW.group_id <> null)) THEN
RAISE EXCEPTION 'The user must belong to the group to add a post';
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_belongs_group_trigger
BEFORE INSERT ON post
FOR EACH ROW
EXECUTE FUNCTION check_belongs_group();
-----------------------------------------
-- (TRIGGER08) A user cannot be friend with himself
CREATE OR REPLACE FUNCTION check_friend_himself() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (NEW.user_id = NEW.friend_id) THEN
RAISE EXCEPTION 'A user cannot send friend request to himself.';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_friend_of_himself
BEFORE INSERT ON friend_request
EXECUTE FUNCTION check_friend_himself();
-----------------------------------------
-- (TRIGGER09) A friend request must only be sent to non-friends
CREATE OR REPLACE FUNCTION check_friendship_exists() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (SELECT * FROM friends where (friend1 = NEW.user_id and friend2 = NEW.friend_id) or
(friend2 = NEW.user_id and friend1 = NEW.friend_id)) THEN
RAISE EXCEPTION 'The users are already friends';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_friendship_exists
BEFORE INSERT ON friend_request
EXECUTE FUNCTION check_friendship_exists();
-----------------------------------------
-- (TRIGGER10) A group request must only be sent to non-members of group
CREATE OR REPLACE FUNCTION check_group_request() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (SELECT * FROM group_user where user_id = NEW.user_id and group_id = NEW.group_id) THEN
RAISE EXCEPTION 'The user is already a member of the group';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_group_request
BEFORE INSERT ON group_request
EXECUTE FUNCTION check_group_request();
-----------------------------------------
-- (TRIGGER11) When a group request is accepted, the user is added to the groupr
CREATE OR REPLACE FUNCTION add_user_to_group() RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.is_accepted = true THEN
INSERT INTO group_user (user_id, group_id) VALUES (NEW.user_id, NEW.group_id);
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER add_user_to_group
AFTER INSERT OR UPDATE ON group_request
FOR EACH ROW
WHEN (NEW.is_accepted = true)
EXECUTE FUNCTION add_user_to_group();
----------------------------------------
-- (TRIGGER12) When a friend request is accepted, the users are now friends
CREATE OR REPLACE FUNCTION add_friend() RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.is_accepted = true THEN
INSERT INTO friends (friend1, friend2) VALUES (NEW.user_id, NEW.friend_id);
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER add_friend
AFTER INSERT OR UPDATE ON friend_request
FOR EACH ROW
WHEN (NEW.is_accepted = true)
EXECUTE FUNCTION add_friend();
INSERT INTO users (id, username, email, password, academic_status, display_name, is_private, role) VALUES
(1, 'johndoe', 'johndoe@example.com', 'password1', 'Undergraduate', 'John Doe', true, 2),
(2, 'alanturing', 'alanturing@example.com', 'password2', 'Professor', 'Alan Turing', false, 2),
(3, 'adalovelace', 'adalovelace@example.com', 'password3', 'Graduate', 'Ada Lovelace', true, 2),
(4, 'admin', 'admin@example.com', 'adminpassword', 'Administrator', 'Admin User', false, 1);
INSERT INTO friend_request(user_id, friend_id, is_accepted, date) VALUES
(2, 3, true, '1940-01-28 12:00:00'),
(1, 4, true, '2023-05-17 15:30:00');
INSERT INTO groups(id, name, description, is_private) VALUES
(1, 'Prolog Enthusiasts', 'A community for discussing Prolog programming language and related topics', false),
(2, 'Tech Enthusiasts', 'A group dedicated to discussing the latest technology trends and innovations', true);
INSERT INTO group_user (user_id, group_id) VALUES
(1, 2),
(1, 1),
(2, 1),
(2, 2);
INSERT INTO group_owner (group_id, user_id) VALUES
(1, 1),
(2, 2);
INSERT INTO post (id, author, title, content, attachment, group_id, is_private, date) VALUES
(1, 1, 'Exciting AI Research Findings', 'Exciting new research findings in the field of artificial intelligence!', 'ai_research.pdf', 1, false, NOW() - INTERVAL '1 day'),
(2, 2, 'Renewable Energy Discussion', 'Important discussion on renewable energy solutions for the future.', 'renewable_energy.png', 2, true, NOW() - INTERVAL '1 day'),
(3, 3, 'Quantum Computing Paper Published', 'Just published my new research paper on quantum computing!', 'quantum_paper.pdf', null, false, NOW() - INTERVAL '3 days'),
(4, 1, 'SpaceXs Mars Colonization Plans', 'Exciting news for all tech enthusiasts - SpaceX plans to colonize Mars!', null, null, true, NOW() - INTERVAL '4 days'),
(5, 2, 'Exploring 6G Technology', 'Discussing the potential of 6G technology and its impact on communication.', null, 2, false, NOW() - INTERVAL '5 days'),
(6, 1, 'History of Computer Science Lecture', 'Attended a fascinating lecture on the history of computer science today.', null, 1, false, NOW() - INTERVAL '6 days'),
(7, 3, 'Challenges in Quantum Computing', 'Exploring the challenges and opportunities in the field of quantum computing.', null, 2, false, NOW() - INTERVAL '7 days'),
(8, 4, 'Future of AI and Society', 'A sneak peek into the future of AI and its implications for society.', null, null, true, NOW() - INTERVAL '8 days'),
(9, 2, 'Python 4.0 Announcement', 'Exciting times ahead for programmers with the launch of the new Python 4.0!', null, 1, false, NOW() - INTERVAL '9 days');
INSERT INTO comment (id, post_id, author, content, date) VALUES
(1, 1, 3, 'This is amazing! Can you share more details about the AI research findings?', NOW() - INTERVAL '1 day'),
(2, 1, 4, 'Im eager to read the research paper. Please share the link when its available!', NOW() - INTERVAL '23 hours'),
(3, 2, 1, 'Renewable energy is the future, and we need to invest more in it.', NOW() - INTERVAL '1 day'),
(4, 4, 3, 'The smartphone industry is advancing rapidly. Any thoughts on sustainability?', NOW() - INTERVAL '2 days'),
(5, 7, 2, 'AIs impact on society is a crucial discussion. Lets explore it further.', NOW() - INTERVAL '2 days'),
(6, 8, 4, 'Python 4.0 sounds exciting! What are the new features in this version?', NOW() - INTERVAL '3 days'),
(7, 5, 1, 'Id love to hear more about the history of computer science. Please share insights!', NOW() - INTERVAL '4 days'),
(8, 3, 2, 'Congratulations on your new research paper on quantum computing!', NOW() - INTERVAL '4 days');
INSERT INTO reaction (post_id, comment_id ,author, type) VALUES
(1, NULL, 3, 'LIKE'),
(NULL, 1, 2, 'HEART'),
(1, NULL, 1, 'LIKE'),
(3, NULL, 2, 'HEART'),
(NULL, 5, 4, 'STAR'),
(6, NULL, 3, 'DISLIKE'),
(NULL, 7, 2, 'DISLIKE');