This section contains the description of the entities and relationships that exist to the onlyFEUP project and its database specification.
The UML diagram in Figure 6 presents the main organizational entities, the relationships between them, attributes and their domains, and the multiplicity of relationships for the OnlyFEUP platform.
Figure 6: OnlyFEUP conceptual data model in UML
Additional business rules or restrictions that cannot be conveyed in the UML class diagram of OnlyFEUP’s system.
**Identifier** | **Description** |
BR07 | A user can only like a post once or like posts from groups to which they belong or like comment in posts from public users or users they follow |
BR08 | A user can only like a comment once |
BR09 | A user can only post to a group that they belong to |
BR10 | A user cannot follow themselves |
BR11 | In post, media and content cant both be null |
BR12 | A user can only comment on posts from public users, posts from users they follow or on posts from groups to which they belong |
BR13 | A group owner is also a member of your group |
BR14 | A user cannot request to follow a user that he/she already follow |
BR15 | A user cannot request to follow themselves |
BR16 | A user cannot request to join a group that he/she is already a part of |
Table 13: Additional Business Rules
This section contains the relational schema obtained from the conceptual data model by analyzing it. It shows each relational schema, attributes, domains, primary keys, foreign keys and other integrity rules necessary such as unique, default, not null and check.
**Relation reference** | **Relation Compact Notation** |
R01 | users(id **PK**, username **UK NN**, password **NN**, email **UK NN**, name **NN**, is_public **NN DF** true, remember_token **DF** null) |
R02 | admin(id->users **PK**) |
R03 | groups(id **PK**, owner_id->users **NN**, name **NN**, is_public **NN DF** false, description) |
R04 | post(id **PK**, owner_id->users **NN**, group_id->groups, content, date **NN CK** date <= today, previous **DF** null) |
R05 | comment(id **PK**, author_id->users **NN**, post_id->post **NN**, content, date **NN CK** date <= today, previous **DF** null) |
R06 | blocked(id->users **PK**) |
R07 | notification(id **PK**, date **NN CK** date <= today, notified_user->users **NN**, emitter_user->users **NN**, viewed **NN DF** false) |
R08 | comment_notification(id->notification **PK**, comment_id->comment) |
R09 | user_notification(id->notification **PK**, notification_type **NN CK** notification_type **IN** user_notification_types) |
R10 | group_notification(id->notification **PK**, group_id->groups **NN**, notification_type **NN CK** notification_type **IN** group_notification_types) |
R11 | post_notification(id->notification **PK**, post_id->post **NN**, notification_type **NN** **CK** notification_type **IN** post_notification_types) |
R12 | member(**user_id**->users, **group_id**->groups, is_favorite **NN DF** false) |
R13 | follow_request(**req_id**->users, **rcv_id**->users) |
R14 | follows(**follower_id**->users, **followed_id**->users) |
R15 | post_likes(**user_id**->users, **post_id**->post) |
R16 | comment_likes(**user_id**->users, **comment_id**->comment) |
R17 | group_join_request(**user_id**->users, **group_id**->groups) |
R18 | configuration(**user_id**->users, notification_type **NN**, active **NN DF** true) |
R19 | message(id **PK**, emitter_id->users, receiver_id->users, content **NN**, date **NN CK** date <= today, viewed **NN DF** false) |
Table 14: OnlyFEUP Relational Schema
Legend:
Specification of additional domains.
**Domain Name** | **Domain Specification** |
**today** | DATE DEFAULT CURRENT_DATE |
**user_notification_types** | ENUM (‘request_follow’, ‘started_following’, ‘accepted_follow’) |
**group_notification_types** | ENUM (‘request_join’, ‘joined_group’, ‘accept_join’) |
**post_notification_types** | ENUM (‘liked_post’, ‘comment_post’) |
**comment_notification_types** | ENUM (‘liked_comment’, ‘comment_post’, ‘reply_comment’, ‘comment_tagging’) |
Table 15: OnlyFEUP Domains
All functional dependencies are identified and the normalization of all relation schemas is accomplished.
**TABLE R01** | **users** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD0101 | {id} → {username, password, email, name, is_public} |
**NORMAL FORM** | BCNF |
Table 16: user schema validation
**TABLE R02** | **admin** |
**Keys** | {id} |
**Functional Dependencies:** | _none_ |
**NORMAL FORM** | BCNF |
Table 17: admin schema validation
**TABLE R03** | **groups** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD0301 | {id} → {owner_id, name, is_public, description} |
**NORMAL FORM** | BCNF |
Table 18: group schema validation
**TABLE R04** | **post** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD0401 | {id} → {user_id, group_id, content, date} |
**NORMAL FORM** | BCNF |
Table 19: post schema validation
**TABLE R05** | **comment** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD0501 | {id} → {author_id, post_id, content, date} |
**NORMAL FORM** | BCNF |
Table 20: comment schema validation
**TABLE R06** | **blocked** |
**Keys** | {id} |
**Functional Dependencies:** | _none_ |
**NORMAL FORM** | BCNF |
Table 21: media schema validation
**TABLE R07** | **notification** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD0701 | {id} → {date, notified_user, emitter_user, viewed} |
**NORMAL FORM** | BCNF |
Table 22: notification schema validation
**TABLE R08** | **comment_notification** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD0801 | {id} → {comment_id} |
**NORMAL FORM** | BCNF |
Table 23: comment_notification schema validation
**TABLE R09** | **user_notification** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD0901 | {id} → {notification_type} |
**NORMAL FORM** | BCNF |
Table 24: user_notification schema validation
**TABLE R10** | **group_notification** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD1001 | {id} → {group_id, notification_type} |
**NORMAL FORM** | BCNF |
Table 25: group_notification schema validation
**TABLE R11** | **post_notification** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD1101 | {id} → {post_id, notification_type} |
**NORMAL FORM** | BCNF |
Table 26: post_notification schema validation
**TABLE R12** | **member** |
**Keys** | {user_id, group_id} |
**Functional Dependencies:** | |
FD1201 | {user_id, group_id} →{is_favorite} |
**NORMAL FORM** | BCNF |
Table 27: member schema validation
**TABLE R13** | **follow_request** |
**Keys** | {req_id, rcv_id} |
**Functional Dependencies:** | _none_ |
**NORMAL FORM** | BCNF |
Table 28: follow_request schema validation
**TABLE R14** | **follows** |
**Keys** | {follower_id, followed_id} |
**Functional Dependencies:** | _none_ |
**NORMAL FORM** | BCNF |
Table 29: follows schema validation
**TABLE R15** | **post_likes** |
**Keys** | {user_id, post_id} |
**Functional Dependencies:** | _none_ |
**NORMAL FORM** | BCNF |
Table 30: post_likes schema validation
**TABLE R16** | **post_likes** |
**Keys** | {user_id, post_id} |
**Functional Dependencies:** | _none_ |
**NORMAL FORM** | BCNF |
Table 31: comment_likes schema validation
**TABLE R17** | **group_join_request** |
**Keys** | {user_id, group_id} |
**Functional Dependencies:** | _none_ |
**NORMAL FORM** | BCNF |
Table 32: group_join_request schema validation
**TABLE R18** | **configuration** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD1801 | {id} -> {user_id, notification_type, active} |
**NORMAL FORM** | BCNF |
Table 33: configuration schema validation
**TABLE R19** | **message** |
**Keys** | {id} |
**Functional Dependencies:** | |
FD1901 | {id} -> {emitter_user, receiver_user, content, date, viewed} |
**NORMAL FORM** | BCNF |
Table 34: message schema validation
Because all relations are in the Boyce–Codd Normal Form (BCNF), the relational schema is also in the BCNF and, therefore, the schema does not need to be further normalized.
The A6 artifact contains the postgres sql code. In other words, It contains the physical schema of the database and its population, the support of data integrity rules with triggers, the identification and characterisation of the indexes and the definition of the database user-defined functions.
It also shows the transactions needed to ensure correctness of the data after accesses and/or changes to the database itself. Furthermore the isolation level is explained as required.
In order to develop a database with good design, it is essential to comprehend the growth of a table and how many times it will be accessed. The table below shows said predictions:
Identifier | Relation Name | Order of magnitude | Estimated growth |
RS01 | user | 10 k | 10 |
RS02 | admin | 1 | 1 |
RS03 | group | 1 k | 10 |
RS04 |
post | 10 k | 1 k |
RS05 | comment | 10 k | 100 |
RS06 | blocked | 100 | 10 |
RS07 | notification | 1 k | 1 k |
RS08 | comment_notification | 100 | 100 |
RS09 | user_notification | 100 | 100 |
RS10 | group_notification | 100 | 100 |
RS11 | post_notification | 100 | 100 |
RS12 | member | 100 | 100 |
RS13 | follow_request | 10 | 10 |
RS14 | follows | 10 | 10 |
RS15 | post_likes | 1 k | 1 k |
RS16 | comment_likes | 1 k | 1 k |
RS17 | group_join_request | 10 | 10 |
RS18 | configuration | 10k | 10 |
RS19 | message | 100k | 1k |
Table 35: OnlyFEUP workload
We used Indexes to increase the database performance by letting It to find and retrieve specific rows in more rapidly ways. An index defined on a column that is part of a join condition can also significantly speed up queries with joins. Moreover, indexes can also benefit UPDATE and DELETE commands with search conditions.
Some queries would normally take a long time to execute. With performance indexes, we can improve the performance of SELECT queries at the expense of increased execution time for INSERT, UPDATE and DELETE operations. Despite this, some tables can benefit from the increased speed in searches. The following tables show the performance indexes used:
Note that the first and second indexes relate to the same queries, the difference being that they relate to different columns in notification, as such they can be considered the same.
Index | IDX01 |
Index relation | notification |
Index attribute | notified_user |
Index type | B-tree |
Cardinality | Medium |
Clustering | Yes |
Justification | Table ‘notification’ is very large. Several queries related to notifications in comments, posts and groups need to frequently find out the user notified. This is done by exact match, thus an hash type index would be best suited. However, since we also want to apply clustering based on this index, and clustering is not possible on hash type indexes, we opted for a b-tree index. Update frequency is low and cardinality is medium so it's a good candidate for clustering. |
SQL code | CREATE INDEX notified_user_notification ON notification USING btree (notified_user); CLUSTER notification USING notified_user_notification; |
Table 36: Notified User Index
Index |
IDX02 |
Index relation |
notification |
Index attributes |
emitter_user |
Index type |
B-tree |
Cardinality |
Medium |
Clustering |
Yes |
Justification |
Table ‘notification’ is very large. Several queries related to notifications in comments, posts and groups need to frequently find out who made a notification . This is done by exact match, thus an hash type index would be best suited. However, since we also want to apply clustering based on this index, and clustering is not possible on hash type indexes, we opted for a b-tree index. Update frequency is low and cardinality is medium so it's a good candidate for clustering.
|
SQL code |
CREATE INDEX emitter_user_notification ON notification USING btree (emitter_user);
CLUSTER notification USING emitter_user_notification;
|
Table 37: Emitter User Index
Index
|
IDX03
|
Index relation
|
post |
Index attributes
|
owner_id
|
Index type
|
Hash |
Cardinality
|
Medium
|
Clustering
|
No |
Justification
|
In a social network, posts are essencial and therefore the table ‘post’ will be quite large. As such, for each post it is frequent and necessary to search for the user who made it. This is done by exact match in those ids. Update frequency is extremely low and cardinality is medium so it's a good candidate for hash tables. |
SQL code
|
CREATE INDEX owner_id_post ON post USING hash (owner_id);
|
Table 38: Post Owner Index
Index
|
IDX04
|
Index relation
|
comment |
Index attributes
|
owner_id
|
Index type
|
Hash |
Cardinality
|
Medium
|
Clustering
|
No |
Justification
|
In a social network, comments are essencial and therefore the table ‘comment’ will be quite large. As such, for each comment is frequent and necessary to search for the user who made it. This is done by exact match in those ids. Update frequency is extremely low and cardinality is medium so it's a good candidate for hash tables
|
SQL code
|
CREATE INDEX owner_id_comment ON comment USING hash (owner_id);
|
Table 39: Comment FTS index
As required by the project’s specifications indexes for full-text search must be developed. Hence, to improve text search time, we created Full-Text Search (FTS) indexes on the tables and attributes we thought would be queried the most. Those indexes can be found in the following tables:
Index
|
IDX05
|
Index relation
|
group |
Index attributes
|
name, description |
Index type
|
GIN |
Clustering
|
No
|
Justification
|
To provide full-text search features to look for groups based on matching names or descriptions. The index type is GIN because the indexed fields are not expected to change often. |
SQL code
|
-- Add column to group to store computed ts_vectors.
ALTER TABLE groupADD
COLUMN tsvectors TSVECTOR;
-- Create a function to automatically update ts_vectors.
CREATE FUNCTION group_search_update() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = (
setweight(to_tsvector('portuguese', NEW.name), 'A') ||
setweight(to_tsvector('portuguese', 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('portuguese', NEW.name), 'A') ||
setweight(to_tsvector('portuguese', NEW.description), 'B')
);
END IF;
END IF;
RETURN NEW;
END $$
LANGUAGE plpgsql;
-- Create a trigger before insert or update on group.
CREATE TRIGGER group_search_update
BEFORE INSERT OR UPDATE ON group
FOR EACH ROW
EXECUTE PROCEDURE group_search_update();
-- Create a GIN index for ts_vectors.
CREATE INDEX search_group ON group USING GIN (tsvectors);
|
Table 40:Group FTS index
Index
|
IDX06
|
Index relation
|
user |
Index attributes
|
name, username |
Index type
|
GIN |
Clustering
|
No
|
Justification
|
To provide full-text search features to look for users based on matching names or usernames. The index type is GIN because the indexed fields are not expected to change often. |
SQL code
|
-- Add column to user to store computed ts_vectors. ALTER TABLE users ADD COLUMN tsvectors TSVECTOR; -- Create a function to automatically update ts_vectors. CREATE FUNCTION user_search_update() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN NEW.tsvectors = ( setweight(to_tsvector('portuguese', NEW.name), 'A') || setweight(to_tsvector('portuguese', NEW.username), 'B') ); END IF; IF TG_OP = 'UPDATE' THEN IF (NEW.name <> OLD.name OR NEW.username <> OLD.username) THEN NEW.tsvectors = ( setweight(to_tsvector('portuguese', NEW.name), 'A') || setweight(to_tsvector('portuguese', NEW.username), 'B') ); END IF; END IF; RETURN NEW; END $$ LANGUAGE plpgsql; -- Create a trigger before insert or update on user CREATE TRIGGER user_search_update BEFORE INSERT OR UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE user_search_update(); -- Create a GIN index for ts_vectors. CREATE INDEX search_user ON users USING GIN (tsvectors); |
Table 41: User FTS index
Index
|
IDX07
|
Index relation
|
post |
Index attributes
|
content |
Index type
|
GIN
|
Clustering
|
No
|
Justification
|
To provide full-text search features to look for posts based on matching content. The index type is GIN because the indexed fields are not expected to change often.
|
SQL code
|
-- Add column to post to store computed ts_vectors.
ALTER TABLE post
ADD COLUMN tsvectors TSVECTOR;
-- Create a function to automatically update ts_vectors.
CREATE FUNCTION post_search_update() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = to_tsvector('portuguese', NEW.content);
END IF;
IF TG_OP = 'UPDATE' THEN
IF (NEW.content <> OLD.content) THEN
NEW.tsvectors = to_tsvector('portuguese', NEW.content);
END IF;
END IF;
RETURN NEW;
END $$
LANGUAGE plpgsql;
-- Create a trigger before insert or update on post
CREATE TRIGGER post_search_update
BEFORE INSERT OR UPDATE ON post
FOR EACH ROW
EXECUTE PROCEDURE post_search_update();
-- Create a GIN index for ts_vectors.
CREATE INDEX search_post ON post USING GIN (tsvectors);
|
Table 42: Post FTS index
Index
|
IDX08
|
Index relation
|
comment |
Index attributes
|
content |
Index type
|
GIN
|
Clustering
|
No
|
Justification
|
To provide full-text search features to look for comments based on matching content. The index type is GIN because the indexed fields are not expected to change often. |
SQL code
|
-- Add column to comment to store computed ts_vectors. ALTER TABLE comment ADD COLUMN tsvectors TSVECTOR; -- Create a function to automatically update ts_vectors. CREATE FUNCTION comment_search_update() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN NEW.tsvectors = to_tsvector('portuguese', NEW.content); END IF; IF TG_OP = 'UPDATE' THEN IF (NEW.content <> OLD.content) THEN NEW.tsvectors = to_tsvector('portuguese', NEW.content); END IF; END IF; RETURN NEW; END $$ LANGUAGE plpgsql; -- Create a trigger before insert or update on comments CREATE TRIGGER comment_search_update BEFORE INSERT OR UPDATE ON comment FOR EACH ROW EXECUTE PROCEDURE comment_search_update(); -- Create a GIN index for ts_vectors. CREATE INDEX search_comment ON comment USING GIN (tsvectors); |
Table 43: Comment FTS index
To enforce integrity rules that cannot be achieved in a simpler way, the necessary triggers are identified and described by presenting the event, the condition, and the activation code. Triggers are also used to keep full-text indexes updated.
Trigger | TRIGGER01 |
Description | A user can only like a post once, or like posts from groups to which they belong or like comments in posts from public users or users they follow (business rule BR07). |
SQL code | CREATE FUNCTION verify_post_likes() RETURNS TRIGGER AS $BODY$ BEGIN IF EXISTS (SELECT \* FROM post_likes WHERE NEW.user_id = user_id AND NEW.post_id = post_id) THEN RAISE EXCEPTION 'A user can only like a post once'; END IF; IF EXISTS (SELECT \* FROM post WHERE NEW.post_id = post.id AND post.group_id IS NOT NULL) AND NOT EXISTS (SELECT \* FROM post,member WHERE NEW.post_id = post.id AND post.group_id = member.group_id AND NEW.user_id = member.user_id) THEN RAISE EXCEPTION 'A user can only like posts from groups to which they belong'; END IF; IF EXISTS (SELECT \* FROM users,post WHERE NEW.post_id = post.id AND post.owner_id = users.id AND NOT users.is_public AND post.group_id IS NULL) AND NOT EXISTS (SELECT \* FROM post,follows WHERE NEW.post_id = post.id AND NEW.user_id = follows.follower_id AND follows.followed_id = post.owner_id) THEN RAISE EXCEPTION 'A user can only like comment posts from public users or users they follow'; END IF; RETURN NEW; END $BODY$ LANGUAGE plpgsql; CREATE TRIGGER verify_post_likes BEFORE INSERT OR UPDATE ON post_likes FOR EACH ROW EXECUTE PROCEDURE verify_post_likes(); |
Table 44: Verify post likes trigger
Trigger |
TRIGGER02 |
Description |
A user can only like a comment once (business rule BR08). |
SQL code |
CREATE FUNCTION verify_comment_likes() RETURNS TRIGGER AS $BODY$ BEGIN IF EXISTS (SELECT \* FROM comment_likes WHERE NEW.user_id = user_id AND NEW.post_id = post_id) THEN RAISE EXCEPTION 'A user can only like a comment once'; END IF; RETURN NEW; END $BODY$ LANGUAGE plpgsql; CREATE TRIGGER verify_comment_likes BEFORE INSERT OR UPDATE ON comment_likes FOR EACH ROW EXECUTE PROCEDURE verify_comment_likes(); |
Table 45: Verify comment likes trigger
Trigger
|
TRIGGER03
|
Description
|
A user can only post to a group that they belong to (business rule BR09). |
SQL code
|
CREATE FUNCTION verify_group_post() RETURNS TRIGGER AS
$BODY$
BEGIN
IF NOT EXISTS (SELECT \* FROM member WHERE NEW.owner_id = user_id AND NEW.group_id = group_id)
AND NEW.group_id IS NOT NULL THEN
RAISE EXCEPTION 'A user can only post to a group that they belong to';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER verify_group_post
BEFORE INSERT OR UPDATE ON post
FOR EACH ROW
EXECUTE PROCEDURE verify_group_post();
|
Table 46: Verify group post trigger
Trigger
|
TRIGGER04
|
Description
|
A user cannot follow themselves (business rule BR10). |
SQL code
|
CREATE FUNCTION verify_self_follow() RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.follower_id = NEW.followed_id THEN
RAISE EXCEPTION 'A user can not follow itself';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER verify_self_follow
BEFORE INSERT OR UPDATE ON follows
FOR EACH ROW
EXECUTE PROCEDURE verify_self_follow();
|
Table 47: Verify self follow trigger
Trigger
|
TRIGGER05
|
Description
|
A user can only comment on posts from public users, posts from users they follow or on posts from groups to which they belong (business rule BR12). |
SQL code
|
CREATE FUNCTION verify_comment() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (SELECT \* FROM post WHERE NEW.post_id = post.id AND post.group_id IS NOT NULL)
AND NOT EXISTS (SELECT \* FROM post,member WHERE NEW.post_id = post.id AND post.group_id = member.group_id AND NEW.owner_id = member.user_id) THEN
RAISE EXCEPTION 'A user can only comment on posts from groups to which they belong';
END IF;
IF EXISTS (SELECT \* FROM users,post WHERE NEW.post_id = post.id AND post.owner_id = users.id AND NOT users.is_public AND post.group_id IS NULL)
AND NOT EXISTS (SELECT \* FROM post,follows WHERE NEW.post_id = post.id AND NEW.owner_id = follows.follower_id AND follows.followed_id = post.owner_id) THEN
RAISE EXCEPTION 'A user can only comment posts from public users or users they follow';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER verify_comment
BEFORE INSERT OR UPDATE ON comment
FOR EACH ROW
EXECUTE PROCEDURE verify_comment();
|
Table 48: Verify comment trigger
Trigger
|
TRIGGER06
|
Description
|
A group owner is also a member of your group (business rule BR13). |
SQL code
|
CREATE FUNCTION group_owner() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO member (user_id, group_id, is_favorite)
VALUES (NEW.owner_id, NEW.id, True);
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER group_owner
BEFORE INSERT OR UPDATE ON groups
FOR EACH ROW
EXECUTE PROCEDURE group_owner();
|
Table 49: Group owner trigger
Trigger
|
TRIGGER07
|
Description
|
A user cannot request to follow a user that he/she already follow (business rule BR14). |
SQL code
|
CREATE FUNCTION check_follow_request() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS
(SELECT \* FROM follows WHERE NEW.req_id = follower_id AND NEW.rcv_id = followed_id)
THEN RAISE EXCEPTION 'Can not make a follow request to someone you already follow';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_follow_request
BEFORE INSERT ON follow_request
FOR EACH ROW
EXECUTE PROCEDURE check_follow_request();
|
Table 50: Follow request trigger
Trigger
|
TRIGGER08
|
Description
|
A user cannot request to follow themselves (business rule BR15).
|
SQL code
|
CREATE FUNCTION verify_self_follow_req() RETURNS TRIGGER AS $BODY$ BEGIN IF NEW.req_id = NEW.rcv_id THEN RAISE EXCEPTION 'A user can not request to follow themselves'; END IF; RETURN NEW; END $BODY$ LANGUAGE plpgsql; CREATE TRIGGER verify_self_follow_req BEFORE INSERT OR UPDATE ON follow_request FOR EACH ROW EXECUTE PROCEDURE verify_self_follow_req(); |
Table 51: Self follow request trigger
Trigger
|
TRIGGER09
|
Description
|
A user cannot request to join a group that he/she is already a part of (business rule BR16).
|
SQL code
|
CREATE FUNCTION check_group_join_req() RETURNS TRIGGER AS $BODY$ BEGIN IF EXISTS (SELECT \* FROM member WHERE NEW.user_id = user_id AND NEW.group_id = group_id) THEN RAISE EXCEPTION 'Can not request to join a group you are already a part of'; END IF; RETURN NEW; END $BODY$ LANGUAGE plpgsql; CREATE TRIGGER check_group_join_req BEFORE INSERT ON group_join_request FOR EACH ROW EXECUTE PROCEDURE check_group_join_req(); |
Table 52: Request join trigger
Trigger
|
TRIGGER10
|
Description
|
When deleting a post it also deletes its comments, subcomments, likes and notifications (business rule BR17)..
|
SQL code
|
CREATE FUNCTION delete_post_action() RETURNS TRIGGER AS $BODY$ BEGIN DELETE FROM post_likes WHERE OLD.id = post_likes.post_id; DELETE FROM post_notification WHERE OLD.id = post_notification.post_id; DELETE FROM comment WHERE OLD.id = comment.post_id; RETURN OLD; END $BODY$ LANGUAGE plpgsql; CREATE TRIGGER delete_post_action BEFORE INSERT ON post FOR EACH ROW EXECUTE PROCEDURE delete_post_action(); |
Table 53: Delete post data trigger
Trigger
|
TRIGGER11
|
Description
|
When deleting a comment it also deletes its likes, subcomments and notifications (business rule BR18)..
|
SQL code
|
CREATE FUNCTION delete_comment_action() RETURNS TRIGGER AS $BODY$ BEGIN DELETE FROM comment_likes WHERE OLD.id = comment_likes.comment_id; DELETE FROM comment_notification WHERE OLD.id = comment_notification.comment_id; DELETE FROM comment WHERE OLD.id = comment.previous; RETURN OLD; END $BODY$ LANGUAGE plpgsql; CREATE TRIGGER delete_comment_action BEFORE INSERT ON comment FOR EACH ROW EXECUTE PROCEDURE delete_comment_action(); |
Table 54: Delete comment data trigger
Trigger
|
TRIGGER12
|
Description
|
When deleting a group it also deletes its posts, members, likes, comments, subcomments, notifications and group_notifications (business rule BR19). .
|
SQL code
|
CREATE FUNCTION delete_group_action() RETURNS TRIGGER AS $BODY$ BEGIN DELETE FROM post WHERE OLD.id = post.group_id; DELETE FROM member WHERE OLD.id = member.group_id; DELETE FROM group_join_request WHERE OLD.id = group_join_request.group_id; DELETE FROM group_notification WHERE OLD.id = group_notification.group_id; RETURN OLD; END $BODY$ LANGUAGE plpgsql; CREATE TRIGGER delete_group_action BEFORE INSERT ON groups FOR EACH ROW EXECUTE PROCEDURE delete_group_action(); |
Table 55: Delete group data trigger
Trigger
|
TRIGGER13
|
Description
|
After deleting a subnotification, delete main notification table entry.
|
SQL code
|
CREATE FUNCTION delete_mainnotification_action() RETURNS TRIGGER AS $BODY$ BEGIN DELETE FROM notification WHERE OLD.id = notification.id; END $BODY$ LANGUAGE plpgsql; CREATE TRIGGER delete_main_post_notification_action AFTER DELETE ON post_notification FOR EACH ROW EXECUTE PROCEDURE delete_mainnotification_action(); CREATE TRIGGER delete_main_comment_notification_action AFTER DELETE ON comment_notification FOR EACH ROW EXECUTE PROCEDURE delete_mainnotification_action(); CREATE TRIGGER delete_main_group_notification_action AFTER DELETE ON group_notification FOR EACH ROW EXECUTE PROCEDURE delete_mainnotification_action(); CREATE TRIGGER delete_main_user_notification_action AFTER DELETE ON user_notification FOR EACH ROW EXECUTE PROCEDURE delete_mainnotification_action(); |
Table 56: Delete notifications trigger
Trigger
|
TRIGGER14
|
Description
|
When a new user appears, he initially gets all kinds of notification (business rule BR20)..
|
SQL code
|
CREATE FUNCTION configuration_action() RETURNS TRIGGER AS $BODY$ BEGIN INSERT INTO configuration (user_id, notification_type) VALUES (NEW.id, 'request_follow'), (NEW.id, 'started_following'), (NEW.id, 'accepted_follow'), (NEW.id, 'requested_join'), (NEW.id, 'joined_group'), (NEW.id, 'accepted_join'), (NEW.id, 'leave_group'), (NEW.id, 'invite'), (NEW.id, 'ban'), (NEW.id, 'group_ownership'), (NEW.id, 'liked_post'), (NEW.id, 'post_tagging'), (NEW.id, 'liked_comment'), (NEW.id, 'comment_post'), (NEW.id, 'reply_comment'), (NEW.id, 'comment_tagging'); RETURN NEW; END $BODY$ LANGUAGE plpgsql; CREATE TRIGGER configuration_action AFTER INSERT ON users FOR EACH ROW EXECUTE PROCEDURE configuration_action(); |
Table 57: User configurations trigger
The transactions below are used to assure the integrity of data when more than one operation is performed and necessary. All transactions below are written in PHP language:
Transaction | TRAN01 |
Description | New comment notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table post_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $post->owner_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $post->owner_id)->get() ->last(); CommentNotification::insert([ 'id' => $newNotification->id, 'comment_id' => $comment->id, 'notification_type' => 'comment_post' ]); $previous = Comment::find($comment->previous); if (!$previous || $previous->owner_id == Auth::user()->id) return redirect()->back()->with('success', 'Comment successfully created'); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $previous->owner_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $previous->owner_id)->get ()->last(); CommentNotification::insert([ 'id' => $newNotification->id, 'comment_id' => $comment->id, 'notification_type' => 'reply_comment' ]); |
Table 58: New comment notification transaction
Transaction | TRAN02 |
Description | New comment like notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table comment_notification and comment_like tables committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
If (CommentLike::where([ 'user_id' => Auth::user()->id, 'comment_id' => $comment->id, ])->exists()) return; CommentLike::insert([ 'user_id' => Auth::user()->id, 'comment_id' => $comment->id, ]); if (Auth::user()->id == $comment->owner_id) return; DB::beginTransaction(); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $comment->owner_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $comment->owner_id)->get()->last(); CommentNotification::insert([ 'id' => $newNotification->id, 'comment_id' => $comment->id, 'notification_type' => 'liked_comment' ]); |
Table 59: New comment like notification transaction
Transaction | TRAN03 |
Description | Remove comment like notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an delete in the table comment_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); CommentLike::where([ 'user_id' => Auth::user()->id, 'comment_id' => $comment->id, ])->delete(); $oldNotification = Notification::leftJoin('comment_notification', 'notification.id', '=', 'comment_notification.id') ->select('notification.id') ->where('notification.emitter_user', Auth::user()->id) ->where('notification.notified_user', $comment->owner_id) ->where('comment_notification.comment_id', $comment->id) ->where('comment_notification.notification_type', 'liked_comment') ->get()->last(); if ($oldNotification) { CommentNotification::where('id', $oldNotification->id)->delete(); Notification::where('id', $oldNotification->id)->delete(); } DB::commit(); |
Table 60: Remove comment like notification transaction
Transaction | TRAN04 |
Description | Remove member from group notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an delete of the notification table or member table could happen, due to an delete in the table group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); Member::where('group_id', $group->id) ->where('user_id', $member->id) ->delete(); Notification::insert([ 'emitter_user' => $group->owner_id, 'notified_user' => $member->id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', $group->owner_id) ->where('notified_user', $member->id)->get()->last(); GroupNotification::insert([ 'id' => $newNotification->id, 'group_id' => $group->id, 'notification_type' => 'ban', ]); DB::commit(); |
Table 61: Remove member from group notification transaction
Transaction | TRAN05 |
Description | Join group notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an insert of the group_notification table could happen, due to an insert in the table member committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); Member::insert([ 'user_id' => Auth::user()->id, 'group_id' => $group->id, 'is_favorite' => false ]); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $group->owner_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $group->owner_id)->get()->last(); GroupNotification::insert([ 'id' => $newNotification->id, 'group_id' => $group->id, 'notification_type' => 'joined_group', ]); DB::commit(); |
Table 62: Join group notification transaction
Transaction | TRAN06 |
Description | Leave group notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an delete/insert of the notification or group_notification tables could happen, due to an insert in the table group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); Member::where('group_id', $group->id) ->where('user_id', Auth::user()->id)->delete(); $oldNotification = Notification::leftJoin('group_notification', 'notification.id', '=', 'group_notification.id') ->select('notification.id') ->where('notification.emitter_user', Auth::user()->id) ->where('notification.notified_user', $group->owner_id) ->where('group_notification.notification_type', 'joined_group') ->get()->last(); if ($oldNotification) { GroupNotification::where('id', $oldNotification->id)->delete(); Notification::where('id', $oldNotification->id)->delete(); } Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $group->owner_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $group->owner_id->get()->last(); GroupNotification::insert([ 'id' => $newNotification->id, 'group_id' => $group->id, 'notification_type' => 'leave_group', ]); DB::commit(); |
Table 63: Leave group notification transaction
Transaction | TRAN07 |
Description | Do join request notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table group_notification or group_join_request table committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); GroupJoinRequest::insert([ 'user_id' => Auth::user()->id, 'group_id' => $group->id ]); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $group->owner_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $group->owner_id)->get()->last(); GroupNotification::insert([ 'id' => $newNotification->id, 'group_id' => $group->id, 'notification_type' => 'requested_join', ]); DB::commit(); |
Table 64:Do join request notification transaction
Transaction | TRAN08 |
Description | Cancel join request |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an delete in the table group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); GroupJoinRequest::where([ 'user_id' => Auth::user()->id, 'group_id' => $group->id ])->delete(); $oldNotification = Notification::leftJoin('group_notification', 'notification.id', '=', 'group_notification.id') ->select('notification.id') ->where('notification.emitter_user', Auth::user()->id) ->where('notification.notified_user', $group->owner_id) ->where('group_notification.notification_type', 'requested_join') ->get()->last(); GroupNotification::where('id', $oldNotification->id)->delete(); Notification::where('id', $oldNotification->id)->delete(); DB::commit(); |
Table 65: Cancel join request transaction
Transaction | TRAN09 |
Description | Accept join request |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); GroupJoinRequest::where([ 'user_id' => $request->user_id, 'group_id' => $group->id ])->delete(); $oldNotification = Notification::leftJoin('group_notification', 'notification.id', '=', 'group_notification.id') ->select('notification.id') ->where('notification.emitter_user', $request->user_id) ->where('notification.notified_user', Auth::user()->id) ->where('group_notification.notification_type', 'requested_join') ->get()->last(); GroupNotification::where('group_notification.id', $oldNotification->id) ->update(['group_notification.notification_type' => 'joined_group']); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $request->user_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $request->user_id)->get()->last(); GroupNotification::insert([ 'id' => $newNotification->id, 'group_id' => $group->id, 'notification_type' => 'accepted_join', ]); Member::insert([ 'group_id' => $group->id, 'user_id' => $request->user_id, ]); DB::commit(); |
Table 66: Accept join request transaction
Transaction | TRAN10 |
Description | Reject Join Request Notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an delete in the table group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); GroupJoinRequest::where([ 'user_id' => $request->user_id, 'group_id' => $group->id ])->delete(); $oldNotification = Notification::leftJoin('group_notification', 'notification.id', '=', 'group_notification.id') ->select('notification.id') ->where('notification.emitter_user', $request->user_id) ->where('notification.notified_user', $group->owner_id) ->where('group_notification.notification_type', 'requested_join') ->get()->last(); if ($oldNotification) { GroupNotification::where('id', $oldNotification->id)->delete(); Notification::where('id', $oldNotification->id)->delete(); } DB::commit(); |
Table 67: Reject Join Request Notification transaction
Transaction | TRAN11 |
Description | Invite notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); Notification::insert([ 'emitter_user' => $group->owner_id, 'notified_user' => $request->user_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', $group->owner_id) ->where('notified_user', $request->user_id)->get()->last(); GroupNotification::insert([ 'id' => $newNotification->id, 'group_id' => $group->id, 'notification_type' => 'invite', ]); DB::commit(); |
Table 68: Invite notification transaction
Transaction | TRAN12 |
Description | Cancel Invite Notification |
Justification | The isolation level is Repeatable Read, because, otherwise, delete of the notification table could happen, due to an delete in the table group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); $oldNotification = Notification::leftJoin('group_notification', 'notification.id', '=', 'group_notification.id') ->select('notification.id') ->where('notification.emitter_user', $group->owner_id) ->where('notification.notified_user', $request->user_id) ->where('group_notification.notification_type', 'invite') ->get()->last(); if ($oldNotification) { GroupNotification::where('id', $oldNotification->id)->delete(); Notification::where('id', $oldNotification->id)->delete(); } DB::commit(); |
Table 69: Cancel Invite Notification transaction
Transaction | TRAN13 |
Description | Reject Invite Request |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an delete in the table group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); $oldNotification = Notification::leftJoin('group_notification', 'notification.id', '=', 'group_notification.id') ->select('notification.id') ->where('notification.emitter_user', $group->owner_id) ->where('notification.notified_user', Auth::user()->id) ->where('group_notification.notification_type', 'invite') ->get()->last(); if ($oldNotification) { GroupNotification::where('id', $oldNotification->id)->delete(); Notification::where('id', $oldNotification->id)->delete(); } DB::commit(); |
Table 70: Reject Invite Request transaction
Transaction | TRAN14 |
Description | Accept Invite |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table member or group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); $oldNotification = Notification::leftJoin('group_notification', 'notification.id', '=', 'group_notification.id') ->select('notification.id') ->where('notification.emitter_user', $group->owner_id) ->where('notification.notified_user', Auth::user()->id) ->where('group_notification.notification_type', 'invite') ->get()->last(); GroupNotification::where('id', $oldNotification->id)->delete(); Notification::where('id', $oldNotification->id)->delete(); Member::insert([ 'group_id' => $group->id, 'user_id' => Auth::user()->id ]); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $group->owner_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $group->owner_id)->get()->last(); GroupNotification::insert([ 'id' => $newNotification->id, 'group_id' => $group->id, 'notification_type' => 'joined_group', ]); DB::commit(); |
Table 71: Accept Invite transaction
Transaction | TRAN15 |
Description | Give ownership notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table and member table could happen, due to an insert in the table group_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); $group->owner_id = $request->input('member_id'); $group->save(); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $request->member_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $request->member_id)->get()->last(); GroupNotification::insert([ 'id' => $newNotification->id, 'group_id' => $group->id, 'notification_type' => 'group_ownership', ]); DB::commit(); |
Table 72: Give ownership notification transaction
Transaction | TRAN16 |
Description | Like post notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table post_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
If (PostLike::where([ 'user_id' => Auth::user()->id, 'post_id' => $post->id, ])->exists()) return; PostLike::insert([ 'user_id' => Auth::user()->id, 'post_id' => $post->id, ]); if (Auth::user()->id == $post->owner_id) return; DB::beginTransaction(); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $post->owner_id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $post->owner_id)->get() ->last(); PostNotification::insert([ 'id' => $newNotification->id, 'post_id' => $post->id, 'notification_type' => 'liked_post' ]); DB::commit(); |
Table 73: Like post notification transaction
Transaction | TRAN17 |
Description | Unlike post notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table post_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); PostLike::where([ 'user_id' => Auth::user()->id, 'post_id' => $post->id, ])->delete(); $oldNotification = Notification::leftJoin('post_notification', 'notification.id', '=', 'post_notification.id') ->select('notification.id') ->where('notification.emitter_usAuth::user()->id) ->where('notificnotified_user', $post->owner_id) ->where('post_notification.post_$post->id) ->where('post_notificanotification_type', 'liked_post') ->get()->last(); if ($oldNotification) { PostNotification::where('id', $oldNotification->id)->delete(); Notification::where('id', $oldNotification->id)->delete(); } DB::commit(); |
Table 74: Unlike post notification transaction
Transaction | TRAN18 |
Description | Delete user |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an delete in many tables committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); // Update user information $user->name = "deleted"; $user->username = "deleted" . $id; $user->password = "deleted" . $id; $user->email = "deleted" . $id; $user->description = ""; $user->is_public = false; // Block user Blocked::insert(['id' => $id]); // Delete user photo ImageController::delete($id, 'profile'); // Delete follows Follow::where('followed_id', $id)->orWhere('follower_id', $id)->delet(); RequestFollow::where('rcv_id', $id)->delete(); // Delete Notifications Notification::where('notified_user', $id)->delete(); // Delete Configurations Configuration::where('user_id', $id)->delete(); // Delete groups (and members, photos, posts, comments, subcomments,invites) $groups = Group::where('owner_id', $id)->get(); foreach ($groups as $group) { $group->delete(); ImageController::delete($group->id, 'groups'); } $user->save(); DB::commit(); |
Table 75: Delete user transaction
Transaction | TRAN19 |
Description | Follow notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table user_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); Follow::insert([ 'follower_id' => Auth::user()->id, 'followed_id' => $request->id, ]); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $request->id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $request->id)->get()->last(); UserNotification::insert([ 'id' => $newNotification->id, 'notification_type' => 'started_following', ]); DB::commit(); |
Table 76: Follow notification transaction
Transaction | TRAN20 |
Description | Unfollow notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table user_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); Follow::where('followed_id', $request->id) ->where('follower_id', Auth::user()->id)->delete(); $oldNotification = Notification::join('user_notification', 'notification.id', '=', 'user_notification.id') ->select('notification.id') ->where('notification.emitter_user', Auth::user()->id) ->where('notification.notified_user', $request->id) ->where('user_notification.notification_type', 'started_following') ->get()->last(); if ($oldNotification) { UserNotification::where('id', $oldNotification->id)->delete(); Notification::where('id', $oldNotification->id)->delete(); } DB::commit(); |
Table 77: Unfollow notification transaction
Transaction | TRAN21 |
Description | Do follow request notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table user_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); RequestFollow::insert([ 'req_id' => Auth::user()->id, 'rcv_id' => $request->id, ]); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $request->id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $request->id)->get()->last(); UserNotification::insert([ 'id' => $newNotification->id, 'notification_type' => 'request_follow', ]); DB::commit(); |
Table 78: Do follow request notification transaction
Transaction | TRAN22 |
Description | Cancel follow request notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table user_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); RequestFollow::where('req_id', Auth::user()->id) ->where('rcv_id', $request->id)->delete(); $oldNotification = Notification::join('user_notification', 'notification.id', '=', 'user_notification.id') ->select('notification.id') ->where('notification.emitter_user', Auth::user()->id) ->where('notification.notified_user', $request->id) ->where('user_notification.notification_type', 'request_follow') ->get()->last(); UserNotification::where('id', $oldNotification->id)->delete(); DB::commit(); |
Table 79: Cancel follow request notification transaction
Transaction | TRAN23 |
Description | Accept follow request notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table user_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); RequestFollow::where('req_id', Auth::user()->id) ->where('rcv_id', $request->id)->delete(); Follow::insert([ 'follower_id' => $request->id, 'followed_id' => Auth::user()->id, ]); $oldNotification = Notification::join('user_notification', 'notification.id', '=', 'user_notification.id') ->select('notification.id') ->where('notification.emitter_user', $request->id) ->where('notification.notified_user', Auth::user()->id) ->where('user_notification.notification_type', 'request_follow') ->get()->last(); UserNotification::where('user_notification.id', $oldNotification->id) ->update(['user_notification.notification_type' => 'started_following']); Notification::insert([ 'emitter_user' => Auth::user()->id, 'notified_user' => $request->id, 'date' => date('Y-m-d H:i'), 'viewed' => false, ]); $newNotification = Notification::select('notification.id') ->where('emitter_user', Auth::user()->id) ->where('notified_user', $request->id)->get()->last(); UserNotification::insert([ 'id' => $newNotification->id, 'notification_type' => 'accepted_follow', ]); DB::commit(); |
Table 80: Accept follow request notification transaction
Transaction | TRAN24 |
Description | Reject Follow Request |
Justification | The isolation level is Repeatable Read, because, otherwise, an update of the notification table could happen, due to an insert in the table user_notification committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); RequestFollow::where('req_id', $request->id) ->where('rcv_id', Auth::user()->id)->delete(); $oldNotification = Notification::join('user_notification', 'notification.id', '=', 'user_notification.id') ->select('notification.id') ->where('notification.emitter_user', $request->id) ->where('notification.notified_user', Auth::user()->id) ->where('user_notification.notification_type', 'request_follow') ->get()->last(); UserNotification::where('id', $oldNotification->id)->delete(); DB::commit(); |
Table 81: Reject Follow Request transaction
Transaction | TRAN25 |
Description | Delete main notification |
Justification | The isolation level is Repeatable Read, because, otherwise, an update or insert of the notification table could happen, due to a delete in the main table notification or others generalization committed by a concurrent transaction, and as a result, inconsistent data would be stored. |
Isolation level | REPEATABLE READ |
DB::beginTransaction(); CommentNotification::where('id', $request->id)->delete(); UserNotification::where('id', $request->id)->delete(); GroupNotification::where('id', $request->id)->delete(); PostNotification::where('id', $request->id)->delete(); Notification::where('id', $request->id)->delete(); DB::commit(); |
Table 83: Delete main notification transaction
OnlyFEUP Database Schema is available in Annexes and in the main code repository.
OnlyFEUP Database Population is available in Annexes and in the main code repository.
create schema if not exists lbaw2255;
SET DateStyle TO European;
-----------------------------------------
-- Drop old schema
-----------------------------------------
DROP TABLE IF EXISTS group_join_request CASCADE;
DROP TABLE IF EXISTS comment_notification CASCADE;
DROP TABLE IF EXISTS user_notification CASCADE;
DROP TABLE IF EXISTS group_notification CASCADE;
DROP TABLE IF EXISTS post_notification CASCADE;
DROP TABLE IF EXISTS member CASCADE;
DROP TABLE IF EXISTS blocked CASCADE;
DROP TABLE IF EXISTS follow_request CASCADE;
DROP TABLE IF EXISTS follows CASCADE;
DROP TABLE IF EXISTS post_likes CASCADE;
DROP TABLE IF EXISTS comment_likes CASCADE;
DROP TABLE IF EXISTS admin CASCADE;
DROP TABLE IF EXISTS notification CASCADE;
DROP TABLE IF EXISTS comment CASCADE;
DROP TABLE IF EXISTS post CASCADE;
DROP TABLE IF EXISTS groups CASCADE;
DROP TABLE IF EXISTS users CASCADE;
DROP TABLE IF EXISTS configuration CASCADE;
DROP TABLE IF EXISTS message CASCADE;
DROP TYPE IF EXISTS user_notification_types;
DROP TYPE IF EXISTS group_notification_types;
DROP TYPE IF EXISTS post_notification_types;
DROP TYPE IF EXISTS comment_notification_types;
DROP FUNCTION IF EXISTS group_search_update CASCADE;
DROP FUNCTION IF EXISTS user_search_update CASCADE;
DROP FUNCTION IF EXISTS comment_search_update CASCADE;
DROP FUNCTION IF EXISTS post_search_update CASCADE;
DROP FUNCTION IF EXISTS verify_post_likes CASCADE;
DROP FUNCTION IF EXISTS verify_comment_likes CASCADE;
DROP FUNCTION IF EXISTS verify_group_post CASCADE;
DROP FUNCTION IF EXISTS verify_comment CASCADE;
DROP FUNCTION IF EXISTS group_owner CASCADE;
DROP FUNCTION IF EXISTS check_follow_request CASCADE;
DROP FUNCTION IF EXISTS verify_self_follow_req CASCADE;
DROP FUNCTION IF EXISTS check_group_join_req CASCADE;
DROP FUNCTION IF EXISTS verify_self_follow CASCADE;
DROP FUNCTION IF EXISTS delete_post_action CASCADE;
DROP FUNCTION IF EXISTS delete_comment_action CASCADE;
DROP FUNCTION IF EXISTS delete_group_action CASCADE;
DROP FUNCTION IF EXISTS delete_user_action CASCADE;
DROP FUNCTION IF EXISTS delete_mainnotification_action CASCADE;
DROP FUNCTION IF EXISTS configuration_action CASCADE;
-----------------------------------------
-- Types
-----------------------------------------
CREATE TYPE user_notification_types AS ENUM ('request_follow', 'started_following', 'accepted_follow');
CREATE TYPE group_notification_types AS ENUM ('requested_join', 'joined_group', 'accepted_join', 'leave_group', 'invite', 'ban', 'group_ownership');
CREATE TYPE post_notification_types AS ENUM ('liked_post', 'post_tagging');
CREATE TYPE comment_notification_types AS ENUM ('liked_comment', 'comment_post', 'reply_comment', 'comment_tagging');
-----------------------------------------
-- Tables
-----------------------------------------
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(256) UNIQUE NOT NULL,
password VARCHAR(256) NOT NULL,
email VARCHAR(256) UNIQUE NOT NULL,
name VARCHAR(256) NOT NULL,
description VARCHAR(512),
is_public BOOLEAN NOT NULL DEFAULT TRUE,
remember_token VARCHAR(256) DEFAULT NULL
);
CREATE TABLE admin (
id INTEGER PRIMARY KEY REFERENCES users (id) ON UPDATE CASCADE
);
CREATE TABLE groups (
id SERIAL PRIMARY KEY,
owner_id INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE,
name VARCHAR(256) UNIQUE NOT NULL,
description VARCHAR(256),
is_public BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE post (
id SERIAL PRIMARY KEY,
owner_id INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE,
group_id INTEGER REFERENCES groups (id) ON UPDATE CASCADE,
is_public BOOLEAN NOT NULL DEFAULT TRUE,
content VARCHAR(256),
date TIMESTAMP NOT NULL CHECK (date <= now())
);
CREATE TABLE comment (
id SERIAL PRIMARY KEY,
owner_id INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE,
post_id INTEGER NOT NULL REFERENCES post (id) ON UPDATE CASCADE,
previous INTEGER DEFAULT NULL REFERENCES comment (id) ON UPDATE CASCADE,
content VARCHAR(256) NOT NULL,
date TIMESTAMP NOT NULL CHECK (date <= now())
);
CREATE TABLE notification (
id SERIAL PRIMARY KEY,
date TIMESTAMP NOT NULL CHECK (date <= now()),
notified_user INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE,
emitter_user INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE,
viewed BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE comment_notification (
id SERIAL PRIMARY KEY REFERENCES notification (id) ON UPDATE CASCADE,
comment_id INTEGER NOT NULL REFERENCES comment (id) ON UPDATE CASCADE,
notification_type comment_notification_types NOT NULL
);
CREATE TABLE user_notification (
id INTEGER PRIMARY KEY REFERENCES notification (id) ON UPDATE CASCADE,
notification_type user_notification_types NOT NULL
);
CREATE TABLE group_notification (
id INTEGER PRIMARY KEY REFERENCES notification (id) ON UPDATE CASCADE,
group_id INTEGER NOT NULL REFERENCES groups (id) ON UPDATE CASCADE,
notification_type group_notification_types NOT NULL
);
CREATE TABLE post_notification (
id INTEGER PRIMARY KEY REFERENCES notification (id) ON UPDATE CASCADE,
post_id INTEGER NOT NULL REFERENCES post (id) ON UPDATE CASCADE,
notification_type post_notification_types NOT NULL
);
CREATE TABLE member (
user_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
group_id INTEGER REFERENCES groups (id) ON UPDATE CASCADE,
is_favorite BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (user_id, group_id)
);
CREATE TABLE follow_request (
req_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
rcv_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
PRIMARY KEY (req_id, rcv_id)
);
CREATE TABLE follows (
follower_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
followed_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
PRIMARY KEY (follower_id, followed_id)
);
CREATE TABLE group_join_request (
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 post_likes (
user_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
post_id INTEGER REFERENCES post (id) ON UPDATE CASCADE,
PRIMARY KEY (user_id, post_id)
);
CREATE TABLE comment_likes (
user_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
comment_id INTEGER REFERENCES comment (id) ON UPDATE CASCADE,
PRIMARY KEY (user_id, comment_id)
);
CREATE TABLE blocked (
id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
PRIMARY kEY (id)
);
CREATE TABLE configuration (
user_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
notification_type VARCHAR(20) NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE message (
id SERIAL PRIMARY KEY,
emitter_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
receiver_id INTEGER REFERENCES users (id) ON UPDATE CASCADE,
content VARCHAR(256) NOT NULL,
date TIMESTAMP NOT NULL CHECK (date <= now()),
viewed BOOLEAN NOT NULL DEFAULT FALSE
);
-----------------------------------------
-- INDEXES
-----------------------------------------
CREATE INDEX notified_user_notification ON notification USING btree (notified_user);
CLUSTER notification USING notified_user_notification;
CREATE INDEX emitter_user_notification ON notification USING btree (emitter_user);
CLUSTER notification USING emitter_user_notification;
CREATE INDEX owner_id_post ON post USING hash (owner_id);
CREATE INDEX owner_id_comment ON comment USING hash (owner_id);
-----------------------------------------
-- FTS INDEXES
-----------------------------------------
-- Add column to group to store computed ts_vectors.
ALTER TABLE groups
ADD COLUMN tsvectors TSVECTOR;
-- Create a function to automatically update ts_vectors.
CREATE FUNCTION group_search_update() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = (
setweight(to_tsvector('simple', NEW.name), 'A') ||
setweight(to_tsvector('simple', 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('simple', NEW.name), 'A') ||
setweight(to_tsvector('simple', NEW.description), 'B')
);
END IF;
END IF;
RETURN NEW;
END $$
LANGUAGE plpgsql;
-- Create a trigger before insert or update on group
CREATE TRIGGER group_search_update
BEFORE INSERT OR UPDATE ON groups
FOR EACH ROW
EXECUTE PROCEDURE group_search_update();
-- Create a GIN index for ts_vectors.
CREATE INDEX search_group ON groups USING GIN (tsvectors);
-- Add column to user to store computed ts_vectors.
ALTER TABLE users
ADD COLUMN tsvectors TSVECTOR;
-- Create a function to automatically update ts_vectors.
CREATE FUNCTION user_search_update() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = (
setweight(to_tsvector('simple', NEW.name), 'A') ||
setweight(to_tsvector('simple', NEW.username), 'B')
);
END IF;
IF TG_OP = 'UPDATE' THEN
IF (NEW.name <> OLD.name OR NEW.username <> OLD.username) THEN
NEW.tsvectors = (
setweight(to_tsvector('simple', NEW.name), 'A') ||
setweight(to_tsvector('simple', NEW.username), 'B')
);
END IF;
END IF;
RETURN NEW;
END $$
LANGUAGE plpgsql;
-- Create a trigger before insert or update on user
CREATE TRIGGER user_search_update
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW
EXECUTE PROCEDURE user_search_update();
-- Create a GIN index for ts_vectors.
CREATE INDEX search_user ON users USING GIN (tsvectors);
-- Add column to post to store computed ts_vectors.
ALTER TABLE post
ADD COLUMN tsvectors TSVECTOR;
-- Create a function to automatically update ts_vectors.
CREATE FUNCTION post_search_update() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = to_tsvector('simple', NEW.content);
END IF;
IF TG_OP = 'UPDATE' THEN
IF (NEW.content <> OLD.content) THEN
NEW.tsvectors = to_tsvector('simple', NEW.content);
END IF;
END IF;
RETURN NEW;
END $$
LANGUAGE plpgsql;
-- Create a trigger before insert or update on post
CREATE TRIGGER post_search_update
BEFORE INSERT OR UPDATE ON post
FOR EACH ROW
EXECUTE PROCEDURE post_search_update();
-- Create a GIN index for ts_vectors.
CREATE INDEX search_post ON post USING GIN (tsvectors);
-- Add column to comment to store computed ts_vectors.
ALTER TABLE comment
ADD COLUMN tsvectors TSVECTOR;
-- Create a function to automatically update ts_vectors.
CREATE FUNCTION comment_search_update() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.tsvectors = to_tsvector('simple', NEW.content);
END IF;
IF TG_OP = 'UPDATE' THEN
IF (NEW.content <> OLD.content) THEN
NEW.tsvectors = to_tsvector('simple', NEW.content);
END IF;
END IF;
RETURN NEW;
END $$
LANGUAGE plpgsql;
-- Create a trigger before insert or update on comments
CREATE TRIGGER comment_search_update
BEFORE INSERT OR UPDATE ON comment
FOR EACH ROW
EXECUTE PROCEDURE comment_search_update();
-- Create a GIN index for ts_vectors.
CREATE INDEX search_comment ON comment USING GIN (tsvectors);
-----------------------------------------
-- TRIGGERS
-----------------------------------------
-- TRIGGER01
-- A user can only like a post once, or like posts from groups to which they belong or like comment in posts from public users or users they follow (business rule BR07)
CREATE FUNCTION verify_post_likes() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (SELECT * FROM post_likes WHERE NEW.user_id = user_id AND NEW.post_id = post_id) THEN
RAISE EXCEPTION 'A user can only like a post once';
END IF;
IF EXISTS (SELECT * FROM post WHERE NEW.post_id = post.id AND post.group_id IS NOT NULL)
AND NOT EXISTS (SELECT * FROM post,member WHERE NEW.post_id = post.id AND post.group_id = member.group_id
AND NEW.user_id = member.user_id) THEN
RAISE EXCEPTION 'A user can only like posts from groups to which they belong';
END IF;
IF EXISTS (SELECT * FROM users,post WHERE NEW.post_id = post.id AND post.owner_id = users.id AND NOT users.is_public AND post.group_id IS NULL AND NEW.user_id <> post.owner_id)
AND NOT EXISTS (SELECT * FROM post,follows WHERE NEW.post_id = post.id AND NEW.user_id = follows.follower_id AND follows.followed_id = post.owner_id) THEN
RAISE EXCEPTION 'A user can only like posts from public users or users they follow';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER verify_post_likes
BEFORE INSERT OR UPDATE ON post_likes
FOR EACH ROW
EXECUTE PROCEDURE verify_post_likes();
-- TRIGGER02
-- A user can only like a comment once (business rule BR08)
CREATE FUNCTION verify_comment_likes() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS
(SELECT * FROM comment_likes WHERE NEW.user_id = user_id AND NEW.comment_id = comment_id)
THEN RAISE EXCEPTION 'A user can only like a comment once';
END IF;
IF EXISTS
(SELECT * FROM post,comment WHERE NEW.comment_id = comment.id AND comment.post_id = post.id AND post.group_id IS NOT NULL)
AND NOT EXISTS
(SELECT * FROM post,member,comment WHERE NEW.comment_id = comment.id AND comment.post_id = post.id AND post.group_id = member.group_id AND NEW.user_id = member.user_id)
THEN RAISE EXCEPTION 'Can not like a comment of a post of a group you do not belong to';
END IF;
IF EXISTS
(SELECT * FROM users,post,comment WHERE NEW.comment_id = comment.id AND comment.post_id = post.id AND post.owner_id = users.id AND NOT users.is_public AND post.group_id IS NULL AND comment.owner_id <> post.owner_id)
AND NOT EXISTS
(SELECT * FROM post,follows,comment WHERE NEW.comment_id = comment.id AND comment.post_id = post.id AND NEW.user_id = follows.follower_id AND follows.followed_id = post.owner_id)
THEN RAISE EXCEPTION 'Can not like comments in posts from private users you do not follow';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER verify_comment_likes
BEFORE INSERT OR UPDATE ON comment_likes
FOR EACH ROW
EXECUTE PROCEDURE verify_comment_likes();
-- TRIGGER03
-- A user can only post to a group that they belong to (business rule BR09)
CREATE FUNCTION verify_group_post() RETURNS TRIGGER AS
$BODY$
BEGIN
IF NOT EXISTS (SELECT * FROM member WHERE NEW.owner_id = user_id AND NEW.group_id = group_id)
AND NEW.group_id IS NOT NULL THEN
RAISE EXCEPTION 'A user can only post to a group that they belong to';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER verify_group_post
BEFORE INSERT OR UPDATE ON post
FOR EACH ROW
EXECUTE PROCEDURE verify_group_post();
-- TRIGGER04
-- A user cannot follow themselves (business rule BR10)
CREATE FUNCTION verify_self_follow() RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.follower_id = NEW.followed_id THEN
RAISE EXCEPTION 'A user can not follow themselves';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER verify_self_follow
BEFORE INSERT OR UPDATE ON follows
FOR EACH ROW
EXECUTE PROCEDURE verify_self_follow();
-- TRIGGER05
-- A user can only comment on posts from public users, posts from users they follow or on posts from groups to which they belong (business rule BR12)
CREATE FUNCTION verify_comment() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (SELECT * FROM post WHERE NEW.post_id = post.id AND post.group_id IS NOT NULL)
AND NOT EXISTS (SELECT * FROM post,member WHERE NEW.post_id = post.id AND post.group_id = member.group_id
AND NEW.owner_id = member.user_id) THEN
RAISE EXCEPTION 'A user can only comment on posts from groups to which they belong';
END IF;
IF EXISTS (SELECT * FROM users,post WHERE NEW.post_id = post.id AND post.owner_id = users.id AND NOT users.is_public AND post.group_id IS NULL AND NEW.owner_id <> post.owner_id)
AND NOT EXISTS (SELECT * FROM post,follows WHERE NEW.post_id = post.id AND NEW.owner_id = follows.follower_id AND follows.followed_id = post.owner_id)
THEN RAISE EXCEPTION 'A user can only comment posts from public users or users they follow';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER verify_comment
BEFORE INSERT OR UPDATE ON comment
FOR EACH ROW
EXECUTE PROCEDURE verify_comment();
-- TRIGGER06
-- A group owner is also a member of your group (business rule BR13)
CREATE FUNCTION group_owner() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO member (user_id, group_id, is_favorite) VALUES (NEW.owner_id, NEW.id, True);
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER group_owner
AFTER INSERT ON groups
FOR EACH ROW
EXECUTE PROCEDURE group_owner();
-- TRIGGER07
-- A user cannot request to follow a user that he/she already follow (business rule BR14)
CREATE FUNCTION check_follow_request() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS
(SELECT * FROM follows WHERE NEW.req_id = follower_id AND NEW.rcv_id = followed_id)
THEN RAISE EXCEPTION 'Can not make a follow request to someone you already follow';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_follow_request
BEFORE INSERT ON follow_request
FOR EACH ROW
EXECUTE PROCEDURE check_follow_request();
-- TRIGGER08
-- A user cannot request to follow themselves (business rule BR15)
CREATE FUNCTION verify_self_follow_req() RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.req_id = NEW.rcv_id THEN
RAISE EXCEPTION 'A user can not request to follow themselves';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER verify_self_follow_req
BEFORE INSERT OR UPDATE ON follow_request
FOR EACH ROW
EXECUTE PROCEDURE verify_self_follow_req();
-- TRIGGER09
-- A user cannot request to join a group that he/she is already a part of (business rule BR16)
CREATE FUNCTION check_group_join_req() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS
(SELECT * FROM member WHERE NEW.user_id = user_id AND NEW.group_id = group_id)
THEN RAISE EXCEPTION 'Can not request to join a group you are already a part of';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER check_group_join_req
BEFORE INSERT ON group_join_request
FOR EACH ROW
EXECUTE PROCEDURE check_group_join_req();
-- TRIGGER10
-- When deleting a post it also deletes its comments, subcomments, likes and notifications (business rule BR17)
CREATE FUNCTION delete_post_action() RETURNS TRIGGER AS
$BODY$
BEGIN
DELETE FROM post_likes WHERE OLD.id = post_likes.post_id;
DELETE FROM post_notification WHERE OLD.id = post_notification.post_id;
DELETE FROM comment WHERE OLD.id = comment.post_id;
RETURN OLD;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER delete_post_action
BEFORE DELETE ON post
FOR EACH ROW
EXECUTE PROCEDURE delete_post_action();
-- TRIGGER11
-- When deleting a comment it also deletes its likes, subcomments and notifications (business rule BR18)
CREATE FUNCTION delete_comment_action() RETURNS TRIGGER AS
$BODY$
BEGIN
DELETE FROM comment_likes WHERE OLD.id = comment_likes.comment_id;
DELETE FROM comment_notification WHERE OLD.id = comment_notification.comment_id;
DELETE FROM comment WHERE OLD.id = comment.previous;
RETURN OLD;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER delete_comment_action
BEFORE DELETE ON comment
FOR EACH ROW
EXECUTE PROCEDURE delete_comment_action();
-- TRIGGER12
-- When deleting a group it also deletes its posts, members, likes, comments, subcomments, notifications and group_notifications (business rule BR19)
CREATE FUNCTION delete_group_action() RETURNS TRIGGER AS
$BODY$
BEGIN
DELETE FROM post WHERE OLD.id = post.group_id;
DELETE FROM member WHERE OLD.id = member.group_id;
DELETE FROM group_join_request WHERE OLD.id = group_join_request.group_id;
DELETE FROM group_notification WHERE OLD.id = group_notification.group_id;
RETURN OLD;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER delete_group_action
BEFORE DELETE ON groups
FOR EACH ROW
EXECUTE PROCEDURE delete_group_action();
-- TRIGGER13
-- After deleting a subnotification, delete main notification table entry
CREATE FUNCTION delete_mainnotification_action() RETURNS TRIGGER AS
$BODY$
BEGIN
DELETE FROM notification WHERE OLD.id = notification.id;
RETURN OLD;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER delete_main_post_notification_action
AFTER DELETE ON post_notification
FOR EACH ROW
EXECUTE PROCEDURE delete_mainnotification_action();
CREATE TRIGGER delete_main_comment_notification_action
AFTER DELETE ON comment_notification
FOR EACH ROW
EXECUTE PROCEDURE delete_mainnotification_action();
CREATE TRIGGER delete_main_group_notification_action
AFTER DELETE ON group_notification
FOR EACH ROW
EXECUTE PROCEDURE delete_mainnotification_action();
CREATE TRIGGER delete_main_user_notification_action
AFTER DELETE ON user_notification
FOR EACH ROW
EXECUTE PROCEDURE delete_mainnotification_action();
-- TRIGGER14
-- When new user appears, he initially gets all kinds of notification (business rule BR20)
CREATE FUNCTION configuration_action() RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO configuration (user_id, notification_type) VALUES
(NEW.id, 'request_follow'),
(NEW.id, 'started_following'),
(NEW.id, 'accepted_follow'),
(NEW.id, 'requested_join'),
(NEW.id, 'joined_group'),
(NEW.id, 'accepted_join'),
(NEW.id, 'leave_group'),
(NEW.id, 'invite'),
(NEW.id, 'ban'),
(NEW.id, 'group_ownership'),
(NEW.id, 'liked_post'),
(NEW.id, 'post_tagging'),
(NEW.id, 'liked_comment'),
(NEW.id, 'comment_post'),
(NEW.id, 'reply_comment'),
(NEW.id, 'comment_tagging');
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER configuration_action
AFTER INSERT ON users
FOR EACH ROW
EXECUTE PROCEDURE configuration_action();
-- Only two examples per table
INSERT INTO users (username, password, email, name, is_public) VALUES
('eduardanascimento', 'c90a22f9bd27177d1c29a848143c791b692b9780', 'eduardanascimento@gmail.com', 'Eduarda Nascimento', TRUE),
('noahpinho', '37dc56b04e264ed9ed48d8a6a53f83d135cc627b', 'noahpinho@fe.up.pt', 'Noah Pinho', TRUE);
INSERT INTO admin (id) VALUES (1), (2);
INSERT INTO groups (owner_id, name, is_public, description) VALUES
(1, 'LBAW G2255', True, 'Work group for lbaw'),
(2, 'Projeto FEUP 22/23', False, 'Engenharia Social');
INSERT INTO member (user_id, group_id) VALUES
(2 , 1),
(3 , 2);
INSERT INTO post (owner_id, group_id, content, date) VALUES
(1, 1, 'I like FEUP!', '8/10/17 0:00'),
(1, 1, 'When is the PFL exam, does someone know???', '4/5/19 0:00');
INSERT INTO comment (owner_id, post_id, content, date) VALUES
(1, 1, 'Great stuff here. Good job', '10/10/22 0:00'),
(2, 3, 'Great stuff here. Good job', '11/10/22 0:00');
INSERT INTO follows (follower_id, followed_id) VALUES
(77, 34),
(16, 3);
INSERT INTO follow_request (req_id, rcv_id) VALUES
(98, 116),
(77, 124);
INSERT INTO group_join_request (user_id, group_id) VALUES
(5, 1),
(34, 2);
INSERT INTO post_likes (user_id,post_id) VALUES
(68, 68),
(10, 119);
INSERT INTO comment_likes (user_id, comment_id) VALUES
(70, 22),
(56, 50);
INSERT INTO message (emitter_id, receiver_id, content, date) VALUES
(4, 1, 'Uma simples mensagem de teste!' ,'29/10/16 0:00'),
(3, 1, 'Uma simples mensagem de teste!' ,'29/10/16 0:00');
October 6:
Started class diagram
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 7:
Continued class diagram. Started additional business rules
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 13:
Finished the class diagram and business rules
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 7:
Started relational schema
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 15:
Finished relational schema. Schema validation and domains.
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 18:
Started SQL schema.
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 18:
Continue SQL schema. Indexes. Started database population.
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 19:
More population.
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 20:
More population. Started performance indexes and FTS indexes.
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 24:
Even more population. Continued triggers.
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 27:
Finished population (except transactions). Finished triggers.
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 29:
Finished all indexes and worked on transactions.
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto
October 30:
Finished transactions and report.
by:
André Costa
Fábio Sá
Lourenço Gonçalves
Marcos Ferreira Pinto