# EBD: Database Specification Component
The Super Legit Collaborative News (SLCN) is a project headed by a small group of developers with the main goal of free, open, and accessible news sharing for and by users.
This will allow all users to view and browse all types of news and comments on any topic, with access to text search and tag selection.
## A4: Conceptual Data Model
The Conceptual Data Model artifact identifies and describes the entities and relations relevant to the database through the use of a UML diagram.
### 1. Class diagram
### 2. Additional Business Rules
- BR07: When a given tag is proposed for the first time, an entry in the TagProposal table and the many-to-many relation table associated with it is created for that tag. When there's another proposal for the same tag, the entry is only created for the many-to-many relation table.
- BR08: A comment can have comments as a reply but those replies can't have comments of their own.
---
## A5: Relational Schema, validation and schema refinement
This artifact contains the Relational Schema created from the Conceptual Model UML. It includes attributes, domains, primary keys, foreign keys and restrictions like UNIQUE, DEFAULT and NOT NULL.
It also includes the schema validations through functional depency analysis.
### 1. Relational Schema
| Relation reference | Relation Compact Notation |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| R01 | user (<u>id</u>, username __UK__ __NN__, email __UK__ __NN__, password __NN__, reputation __NN__ __DF__ 0, country, picture, is_admin __NN__ __DF__ False) |
| R02 | follows(<u>id1</u> -> user, <u>id2</u> -> user) |
| R03 | apply_admin_request(<u>id</u>, description __NN__, is_handled __NN__ __DF__ False, id_user -> user __UK__ __NN__) |
| R04 | news(<u>id</u>, reputation __NN__ __DF__ 0, title __NN__, content __NN__, date __NN__ __DF__ Today, picture, id_author -> user **NN**) |
| R05 | news_favorite(<u>id_user</u> -> user, <u>id_news</u> -> news) |
| R06 | news_vote(<u>id_user</u> -> user, <u>id_news</u> -> news, is_liked **NN**) |
| R07 | news_tag(<u>id_news</u> -> news, <u>id_tag</u> -> tag) |
| R08 | comment(<u>id</u>, reputation __NN__ __DF__ 0, content __NN__, date __NN__ __DF__ Today, id_news -> news __NN__, id_comment -> comment, id_author -> user __NN__) |
| R09 | comment_vote(<u>id_user</u> -> user, <u>id_comment</u> -> comment, is_liked **NN**) |
| R10 | tag(<u>id</u>, tag_name __UK__ __NN__) |
| R11 | tag_follow (<u>id_user</u> -> user, <u>id_tag</u> -> tag) |
| R12 | tag_proposal(<u>id</u>, tag_name __UK__ __NN__, description __NN__, is_handled __NN__ __DF__ False) |
| R13 | tag_proposal_user (<u>id_user</u> -> user, <u>id_tag</u> -> tag_proposal) |
| R14 | report(<u>id_report</u>, report_type **NN**, date **NN** **DF** Today, report_text **NN**, is_handled **NN** **DF** False, id_author -> user **NN**, id_user -> user, id_news -> news, id_comment -> comment)|
| R15 | notification(<u>id_notification</u>, notification_type **NN**, date **NN** **DF** Today, is_viewed **NN** **DF** False, id_user -> user **NN**, id_news -> news, id_comment -> comment)
Legend:
- **UK** = UNIQUE KEY
- **NN** = NOT NULL
- **DF** = DEFAULT
### 2. Domains
| Domain Name | Domain Specification |
| ----------- | ------------------------------ |
| Today | DATE DEFAULT CURRENT_DATE |
| ReportType | ENUM('UserReport', 'NewsReport', 'CommentReport')|
| NotificationType | ENUM('NewsVote', 'CommentVote', 'NewsComment')|
### 3. Schema validation
>
| **TABLE R01** | user |
| -------------- | --- |
| **Keys** | {id}, {email}, {username} |
| **Functional Dependencies:**| |
| FD0101| {id} -> {username, email, password, country, picture, is_admin}|
| FD0102 | {email} -> {id, username, password, reputation, country, picture, is_admin} |
| FD0103 | {username} -> {id, email, password, reputation, country, picture, is_admin}
| **NORMAL FORM** | BCNF |
| **TABLE R02** | follows |
| -------------- | --- |
| **Keys** | {id1, id2}|
| **Functional Dependencies:** | *none* |
| **NORMAL FORM** | BCNF |
| **TABLE R03** | apply_admin_request |
| -------------- | --- |
| **Keys** | {id}, {id_user} |
| **Functional Dependencies:** | |
| FD0401 | {id} → {description, is_handled, id_user}|
| FD0402 | {id_user} -> {id, description, is_handled} |
| **NORMAL FORM** | BCNF |
| **TABLE R04** | news|
| -------------- | --- |
| **Keys** | {id} |
| **Functional Dependencies:** | |
| FD0501 | {id} -> {reputation, title, content, date, picture, id_author} |
| **NORMAL FORM** | BCNF |
| **TABLE R05** | news_favorite |
| -------------- | --- |
| **Keys** | {id_user, id_news}|
| **Functional Dependencies:** | *none* |
| **NORMAL FORM** | BCNF |
| **TABLE R06** | news_vote |
| -------------- | --- |
| **Keys** | {id_user, id_news}|
| **Functional Dependencies:** |
| FD0701 | {id_user, id_news} -> {is_liked} |
| **NORMAL FORM** | BCNF |
| **TABLE R07** | news_tag|
| -------------- | --- |
| **Keys** | {id_news, id_tag}|
| **Functional Dependencies:** | *none* |
| **NORMAL FORM** | BCNF |
| **TABLE R08** | comment |
|--------------- | ---|
| **Keys** | {id} |
|**Functional Dependencies:** |
| FD0901 | {id} -> {reputation, content, date} |
| **NORMAL FORM** | BCNF |
| **TABLE R09** | comment_vote |
| -------------- | --- |
| **Keys** | {id_user, id_comment}|
| **Functional Dependencies:** |
| FD1001 | {id_user, id_comment} -> {is_liked} |
| **NORMAL FORM** | BCNF |
| **TABLE R10** | tag |
| -------------- | --- |
| **Keys** | {id}, {tag_name}|
| **Functional Dependencies:** | |
| FD1101 | {id} -> {name} |
| FD1102 | {tag_name} -> {id}
| **NORMAL FORM** | BCNF |
| **TABLE R11** | tag_follow |
| -------------- | --- |
| **Keys** | {id_user, id_tag}|
| **Functional Dependencies:** | *none* |
| **NORMAL FORM** | BCNF |
| **TABLE R12** | tag_proposal |
| -------------- | --- |
| **Keys** | {id}, {tag_name} |
| **Functional Dependencies:** | |
| FD1201 | {id} -> {tag_name, description, is_handled}
| FD1202 | {tag_name} -> {id, description, is_handled}
| **NORMAL FORM** | BCNF |
| **TABLE R13** | tag_proposal_user |
| -------------- | --- |
| **Keys** | {id_user, id_tag}|
| **Functional Dependencies:** | *none* |
| **NORMAL FORM** | BCNF |
| **TABLE R14** | report |
| --- | --- |
| **Keys** | {id_report} |
| **Functional Dependencies**| |
| FD1401 | {id_report} -> {report_type, date, report_text, is_handled, id_author, id_user, id_news, id_comment} |
| **NORMAL FORM** | BCNF |
| **TABLE R15** | notification |
| -------------- | --- |
| **Keys** | {id_notification}|
| **Functional Dependencies:** | |
| FD1501 | {id_notification} -> {notification_type, date, is_viewed, id_user, id_news, id_comment}|
| **NORMAL FORM** | BCNF |
### Considering all the tables are in the BCNF, the Schema is in the BCNF.
---
## A6: Indexes, triggers, transactions and database population
This artefact contains the Database Workload, the proposed indices, triggers and transactions we created for our database. There is also the complete database creation and population scripts, in the annex.
### 1. Database Workload
| **Relation reference** | **Relation Name** | **Order of magnitude** | **Estimated growth** |
| ------------------ | ------------- | ------------------------- | -------- |
| R01 | user | 10 M | 10 k / day |
| R02 | follows | 100 M | 100 k / day |
| R03 | apply_admin_request | 1 k | 10 / day |
| R04 | news | 1 M | 10 k / day |
| R05 | news_favorite | 100 k | 1 k / day |
| R06 | news_vote | 1 B | 1 M / day |
| R07 | news_tag | 1 M | 10 k / day |
| R08 | comment | 10 M | 100 k / day |
| R09 | comment_vote | 100 M | 1 M / day |
| R10 | tag | 100 | 1 / day |
| R11 | tag_follow | 10 M | 10 k / day |
| R12 | tag_proposal | 10 | 1 / day |
| R13 | tag_proposal_user | 100 k | 100 / day |
| R14 | report | 10 k | 100 / day |
| R15 | notification | 1 B | 1 M / day |
### 2. Proposed Indices
#### 2.1. Performance Indices
| **Index** | IDX01 |
| --- | --- |
| **Relation** | comment |
| **Attribute** | id_news |
| **Type** | Hash |
| **Cardinality** | Medium |
| **Clustering** | No |
| **Justification** | Table ‘comment’ is very large. Everytime we open a news, we need to filter access to the comments by its corresponding news. Filtering is done by exact match, thus an hash type index is best suited. |
**SQL code**
```sql
CREATE INDEX news_comments ON comment USING hash (id_news);
```
| **Index** | IDX02 |
| --- | --- |
| **Relation** | news |
| **Attribute** | reputation |
| **Type** | B-tree |
| **Cardinality** | Medium |
| **Clustering** | No |
| **Justification** | Table ‘news’ is frequently accessed for news filtered by popularity (reputation). A B-tree index allows for faster order search queries based on the reputation. |
**SQL code**
```sql
CREATE INDEX news_by_popularity ON news USING btree (reputation);
```
| **Index** | IDX03 |
| --- | --- |
| **Relation** | notification |
| **Attribute** | id_user |
| **Type** | Hash |
| **Cardinality** | High |
| **Clustering** | No |
| **Justification** | Table 'notification' is very large. Everytime a user sees his notifications, we need to filter access to the notifications by the user they correspond to. Filtering is done by exact match, thus an hash type index is best suited. |
**SQL code**
```sql
CREATE INDEX user_notifications ON notification USING hash (id_user);
```
#### 2.2. Full-text Search Indices
| **Index** | IDX01 |
| --- | --- |
| **Relation** | news |
| **Attribute** | title, content |
| **Type** | GiST |
| **Clustering** | No |
| **Justification** | To provide full-text search features to look for news based on matching titles or content. The index type is GiST because the indexed fields are not expected to change often. |
**SQL code**
```sql
SET search_path TO lbaw2286;
ALTER TABLE news
ADD COLUMN tsvectors TSVECTOR;
CREATE FUNCTION news_search_update() 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 (NEW.title <> OLD.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 news_search_update
BEFORE INSERT OR UPDATE ON news
FOR EACH ROW
EXECUTE PROCEDURE news_search_update();
CREATE INDEX search_news ON news USING GiST (tsvectors);
```
### 3. Triggers
| **Trigger** | TRIGGER01 |
| --- | --- |
| **Description** | Trigger that updates comment and user reputation when a new vote is issued on the comment. BR01 |
**SQL code**
```sql
SET search_path TO lbaw2286;
CREATE FUNCTION add_comment_reputation() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (NEW.is_liked) THEN
UPDATE comment
SET reputation = reputation+1
WHERE id = NEW.id_comment;
UPDATE users
SET reputation = reputation+1
WHERE id = (
SELECT id_author FROM comment WHERE id = NEW.id_comment
);
ELSE
UPDATE comment
SET reputation = reputation-1
WHERE id = NEW.id_comment;
UPDATE users
SET reputation = reputation-1
WHERE id = (
SELECT id_author FROM comment WHERE id = NEW.id_comment
);
END IF;
RETURN NULL;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER add_comment_reputation
AFTER INSERT ON comment_vote FOR EACH ROW
EXECUTE PROCEDURE add_comment_reputation();
```
| **Trigger** | TRIGGER02 |
| --- | --- |
| **Description** | Trigger that updates news and user reputation when a new vote is issued on the news. BR01 |
**SQL code**
```sql
SET search_path TO lbaw2286;
CREATE FUNCTION add_news_reputation() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (NEW.is_liked) THEN
UPDATE news
SET reputation = reputation+1
WHERE id = NEW.id_news;
UPDATE users
SET reputation = reputation+1
WHERE id = (
SELECT id_author FROM news WHERE id = NEW.id_news
);
ELSE
UPDATE news
SET reputation = reputation-1
WHERE id = NEW.id_news;
UPDATE users
SET reputation = reputation-1
WHERE id = (
SELECT id_author FROM news WHERE id = NEW.id_news
);
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER add_news_reputation
AFTER INSERT ON news_vote FOR EACH ROW
EXECUTE PROCEDURE add_news_reputation();
```
| **Trigger** | TRIGGER03 |
| --- | --- |
| **Description** | Trigger that updates a user and comment reputation when a vote is removed from said comment. BR01 |
**SQL code**
```sql
SET search_path TO lbaw2286;
CREATE FUNCTION remove_comment_reputation() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (OLD.is_liked) THEN
UPDATE comment
SET reputation = reputation-1
WHERE id = OLD.id_comment;
UPDATE users
SET reputation = reputation-1
WHERE id = (
SELECT id_author FROM comment WHERE id = NEW.id_comment
);
ELSE
UPDATE comment
SET reputation = reputation+1
WHERE id = OLD.id_comment;
UPDATE users
SET reputation = reputation+1
WHERE id = (
SELECT id_author FROM comment WHERE id = NEW.id_comment
);
END IF;
RETURN NULL;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER remove_comment_reputation
BEFORE DELETE ON comment_vote FOR EACH ROW
EXECUTE PROCEDURE remove_comment_reputation();
```
| **Trigger** | TRIGGER04 |
| --- | --- |
| **Description** | Trigger that updates a user and news reputation when a vote is removed from said news. BR01 |
**SQL code**
```sql
SET search_path TO lbaw2286;
CREATE FUNCTION remove_news_reputation() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (OLD.is_liked) THEN
UPDATE news
SET reputation = reputation-1
WHERE id = OLD.id_news;
UPDATE users
SET reputation = reputation-1
WHERE id = (
SELECT id_author FROM news WHERE id = NEW.id_news
);
ELSE
UPDATE news
SET reputation = reputation+1
WHERE id = OLD.id_news;
UPDATE users
SET reputation = reputation+1
WHERE id = (
SELECT id_author FROM news WHERE id = NEW.id_news
);
END IF;
RETURN NULL;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER remove_news_reputation
BEFORE DELETE ON news_vote FOR EACH ROW
EXECUTE PROCEDURE remove_news_reputation();
```
| **Trigger** | TRIGGER05 |
| --- | --- |
| **Description** | Trigger that updates a user and comment reputation when a vote is updated on said comment. BR01 |
**SQL code**
```sql
SET search_path TO lbaw2286;
CREATE FUNCTION update_comment_reputation() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (NEW.is_liked) THEN
UPDATE comment
SET reputation = reputation+2
WHERE id = NEW.id_comment;
UPDATE users
SET reputation = reputation+2
WHERE id = (
SELECT id_author FROM comment WHERE id = NEW.id_comment
);
ELSE
UPDATE comment
SET reputation = reputation-2
WHERE id = NEW.id_comment;
UPDATE users
SET reputation = reputation-2
WHERE id = (
SELECT id_author FROM comment WHERE id = NEW.id_comment
);
END IF;
RETURN NULL;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_comment_reputation
AFTER UPDATE ON comment_vote FOR EACH ROW
EXECUTE PROCEDURE update_comment_reputation();
```
| **Trigger** | TRIGGER06 |
| --- | --- |
| **Description** | Trigger that updates a user and news reputation when a vote is updated on said news. BR01 |
**SQL code**
```sql
SET search_path TO lbaw2286;
CREATE FUNCTION update_news_reputation() RETURNS TRIGGER AS
$BODY$
BEGIN
IF (NEW.is_liked) THEN
UPDATE news
SET reputation = reputation+2
WHERE id = NEW.id_news;
UPDATE users
SET reputation = reputation+2
WHERE id = (
SELECT id_author FROM news WHERE id = NEW.id_news
);
ELSE
UPDATE news
SET reputation = reputation-2
WHERE id = NEW.id_news;
UPDATE users
SET reputation = reputation-2
WHERE id = (
SELECT id_author FROM news WHERE id = NEW.id_news
);
END IF;
RETURN NULL;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER update_news_reputation
AFTER UPDATE ON news_vote FOR EACH ROW
EXECUTE PROCEDURE update_news_reputation();
```
| **Trigger** | TRIGGER07 |
| --- | --- |
| **Description** | Trigger that replaces user data with anonymous data on user account deletion. BR03 |
**SQL code**
```sql
SET search_path TO lbaw2286;
-- user id 5 is anonymous
CREATE FUNCTION anonymous_user() RETURNS TRIGGER AS
$BODY$
BEGIN
UPDATE news SET id_author=5 WHERE OLD.id = id_author;
UPDATE comment SET id_author=5 WHERE OLD.id = id_author;
UPDATE apply_admin_request SET id_user = 5 WHERE OLD.id = id_user;
RETURN NULL;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER anonymous_user
BEFORE DELETE ON users
FOR EACH ROW
EXECUTE PROCEDURE anonymous_user();
```
| **Trigger** | TRIGGER08 |
| --- | --- |
| **Description** | Trigger that garantees that a comment can't be a reply to another reply. BR08 |
**SQL code**
```sql
SET search_path TO lbaw2286;
CREATE FUNCTION comment_on_comment() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (select id_comment from comment where id_comment = NEW.id) THEN-- se comentário já for resposta a comentário não pode ser comentado
RAISE EXCEPTION 'Comments that are commented on other comment cant have comments';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER comment_on_comment
BEFORE INSERT ON comment
FOR EACH ROW
EXECUTE PROCEDURE comment_on_comment();
```
| **Trigger** | TRIGGER09 |
| --- | --- |
| **Description** | Trigger that garantees that a comment can't be deleted if it has replies or votes. BR02 |
**SQL code**
```sql
SET search_path TO lbaw2286;
CREATE FUNCTION delete_comment() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (SELECT * FROM comment WHERE id_comment = OLD.id ) THEN
RAISE EXCEPTION 'You cant delete a comment with comments in it';
END IF;
IF NOT (OLD.reputation = 0) THEN
RAISE EXCEPTION 'You cant delete a comment with votes in it.';
END IF;
RETURN NULL;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER delete_comment
BEFORE DELETE ON comment
FOR EACH ROW
EXECUTE PROCEDURE delete_comment();
```
| **Trigger** | TRIGGER10 |
| --- | --- |
| **Description** | Trigger that garantees that a news item can't be deleted if it has comments or votes. BR02 |
**SQL code**
```sql
SET search_path TO lbaw2286;
CREATE FUNCTION delete_news() RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS (SELECT * FROM comment WHERE id_news = OLD.id ) THEN
RAISE EXCEPTION 'You cant delete news with comments in it';
END IF;
IF NOT (OLD.reputation = 0) THEN
RAISE EXCEPTION 'You cant delete news with votes in it';
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER delete_news
BEFORE DELETE ON news
FOR EACH ROW
EXECUTE PROCEDURE delete_news();
```
### 4. Transactions
| SQL Reference | newstag |
| --------------- | ----------------------------------- |
| Justification | When news are created, news_tag entries are also created to associate the news and the chosen tags. In the middle of the tansaction, new rows can be inserted in the news table, which could result in currval returning a wrong id. To prevent these non-repeatable reads, we chose isolation level Repeatable Read.|
| Isolation level | REPEATABLE READ |
**Complete SQL Code**
```sql
SET search_path TO lbaw2286;
BEGIN TRANSACTION;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- Insert news
INSERT INTO news(title, content, picture, id_author) VALUES ($title, $content, $picture, $id_author);
-- Insert news_tag
INSERT INTO news_tag(id_news, id_tag) VALUES (currval('news_id_seq'), $id_tag);
END TRANSACTION;
```
| SQL Reference | tagproposal |
| --------------- | ----------------------------------- |
| Justification | When a tag is proposed for the first time, an entry on the tag_proposal table is created, containing the tag information. It is also necessary to create an entry in the tag_proposal_user to associate the user to its proposal. In the middle of the tansaction, new rows can be inserted in the tag_proposal table, which could result in currval returning a wrong id. To prevent these non-repeatable reads, we chose isolation level Repeatable Read.|
| Isolation level | REPEATABLE READ |
**SQL code**
```sql
SET search_path TO lbaw2286;
BEGIN TRANSACTION;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- Insert tag_proposal
INSERT INTO tag_proposal (tag_name, description) VALUES ($tag_name, $description);
-- Insert tag_proposal_user
INSERT INTO tag_proposal_user (id_user, id_tag) VALUES ($id_user, currval('tag_proposal_id_seq'));
END TRANSACTION;
```
## Annex A. SQL Code
### A.1. Database schema
The full database schema creation script can be found [here](https://git.fe.up.pt/lbaw/lbaw2223/lbaw2286/-/blob/main/database/database.sql).
**SQL code**
```sql
-- SCHEMA: lbaw2286
DROP SCHEMA IF EXISTS lbaw2286 CASCADE;
CREATE SCHEMA IF NOT EXISTS lbaw2286
AUTHORIZATION postgres;
SET search_path TO lbaw2286;
-----------------------------------------
-- Drop old schema
-----------------------------------------
DROP TABLE IF EXISTS users CASCADE; --R01
DROP TABLE IF EXISTS follows CASCADE; --R02
DROP TABLE IF EXISTS apply_admin_request CASCADE; --R03
DROP TABLE IF EXISTS news CASCADE; --R04
DROP TABLE IF EXISTS news_favorite CASCADE; --R05
DROP TABLE IF EXISTS news_vote CASCADE; --RO6
DROP TABLE IF EXISTS news_tag CASCADE; --R07
DROP TABLE IF EXISTS comment CASCADE; --R08
DROP TABLE IF EXISTS comment_vote CASCADE; --R09
DROP TABLE IF EXISTS tag CASCADE; --R10
DROP TABLE IF EXISTS tag_follow CASCADE; --R11
DROP TABLE IF EXISTS tag_proposal CASCADE; --R12
DROP TABLE IF EXISTS tag_proposal_user CASCADE; --R13
DROP TABLE IF EXISTS report CASCADE; --R14
DROP TABLE IF EXISTS notification CASCADE; --R15
-----------------------------------------
-- Types
-----------------------------------------
DROP TYPE IF EXISTS ReportType CASCADE;
DROP TYPE IF EXISTS NotificationType CASCADE;
CREATE TYPE ReportType AS ENUM ('UserReport', 'NewsReport', 'CommentReport');
CREATE TYPE NotificationType AS ENUM ('NewsVote', 'CommentVote', 'NewsComment');
-----------------------------------------
-- Tables
-----------------------------------------
-- Note that a plural 'users' name was adopted because user is a reserved word in PostgreSQL.
--R01
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
reputation INTEGER NOT NULL DEFAULT 0,
country TEXT,
picture TEXT,
isAdmin BOOLEAN NOT NULL
);
--R02
CREATE TABLE follows (
id1 INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE,
id2 INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE,
PRIMARY KEY (id1, id2)
);
--R03
CREATE TABLE apply_admin_request (
id SERIAL PRIMARY KEY,
id_user INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE,
description TEXT NOT NULL,
is_handled BOOL NOT NULL DEFAULT False
);
--R10
CREATE TABLE tag(
id SERIAL PRIMARY KEY,
tag_name TEXT UNIQUE NOT NULL
);
--R04
CREATE TABLE news (
id SERIAL PRIMARY KEY,
reputation INTEGER NOT NULL DEFAULT 0,
title TEXT NOT NULL,
content TEXT NOT NULL,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
picture TEXT,
id_author INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE
);
--R05
CREATE TABLE news_favorite (
id_user INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE,
id_news INTEGER NOT NULL REFERENCES news (id) ON UPDATE CASCADE ON DELETE CASCADE,
PRIMARY KEY (id_user, id_news)
);
--R06
CREATE TABLE news_vote (
id_user INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE,
id_news INTEGER NOT NULL REFERENCES news (id) ON UPDATE CASCADE,
is_liked BOOL NOT NULL,
PRIMARY KEY (id_user, id_news)
);
--R07
CREATE TABLE news_tag (
id_news INTEGER NOT NULL REFERENCES news (id) ON UPDATE CASCADE ON DELETE CASCADE,
id_tag INTEGER NOT NULL REFERENCES tag (id) ON UPDATE CASCADE ON DELETE CASCADE,
PRIMARY KEY (id_news, id_tag)
);
--R08
CREATE TABLE comment (
id SERIAL PRIMARY KEY,
reputation INTEGER NOT NULL DEFAULT 0,
content TEXT NOT NULL,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
id_news INTEGER NOT NULL REFERENCES news (id) ON UPDATE CASCADE,
id_author INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE,
id_comment INTEGER REFERENCES comment (id) ON UPDATE CASCADE
);
--R09
CREATE TABLE comment_vote (
id_user INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE,
id_comment INTEGER NOT NULL REFERENCES comment (id) ON UPDATE CASCADE,
is_liked BOOL NOT NULL,
PRIMARY KEY (id_user, id_comment)
);
--R11
CREATE TABLE tag_follow (
id_user INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE,
id_tag INTEGER NOT NULL REFERENCES tag (id) ON UPDATE CASCADE ON DELETE CASCADE,
PRIMARY KEY (id_user, id_tag)
);
--R12
CREATE TABLE tag_proposal (
id SERIAL PRIMARY KEY,
tag_name TEXT UNIQUE NOT NULL,
description TEXT NOT NULL,
is_handled BOOLEAN DEFAULT False
);
--R13
CREATE TABLE tag_proposal_user (
id_user INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE ON DELETE CASCADE,
id_tag INTEGER NOT NULL REFERENCES tag_proposal (id) ON UPDATE CASCADE,
PRIMARY KEY (id_user, id_tag)
);
--R14
CREATE TABLE report (
id_report SERIAL PRIMARY KEY,
report_type ReportType NOT NULL,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
report_text TEXT,
is_handled BOOLEAN DEFAULT False,
id_author INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE,
id_user INTEGER REFERENCES users (id) ON UPDATE CASCADE,
id_news INTEGER REFERENCES news (id) ON UPDATE CASCADE,
id_comment INTEGER REFERENCES comment (id) ON UPDATE CASCADE,
CHECK((id_user IS NOT NULL AND id_news IS NULL AND id_comment IS NULL) OR (id_user IS NULL AND id_news IS NOT NULL AND id_comment IS NULL) OR (id_user IS NULL AND id_news IS NULL AND id_comment IS NOT NULL))
);
--R15
CREATE TABLE notification (
id_notification SERIAL PRIMARY KEY,
notification_type NotificationType NOT NULL,
date TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
is_viewed BOOLEAN NOT NULL DEFAULT False,
id_user INTEGER NOT NULL REFERENCES users (id) ON UPDATE CASCADE,
id_news INTEGER REFERENCES news (id) ON UPDATE CASCADE,
id_comment INTEGER REFERENCES comment (id) ON UPDATE CASCADE,
CHECK((id_news IS NOT NULL AND id_comment IS NULL) OR (id_news IS NULL AND id_comment IS NOT NULL))
);
```
### A.2. Database population
The full database schema population script can be found [here](https://git.fe.up.pt/lbaw/lbaw2223/lbaw2286/-/blob/main/database/populate.sql).
**SQL code**
```sql
-------------------------------
-- Users
-------------------------------
SET search_path TO lbaw2286;
INSERT INTO users (id, username, email, password, country, picture, isAdmin) VALUES(1, 'André Morais', 'andre@legitmail.com', 'legitandre', 'Portugal', './path/to/picture.png', true);
INSERT INTO users (id, username, email, password, country, picture, isAdmin) VALUES(2, 'João Teixeira', 'joao@legitmail.com', 'legitjoao', 'Portugal', './path/to/picture.png', true);
INSERT INTO users (id, username, email, password, country, picture, isAdmin) VALUES(3, 'Lucas Sousa', 'lucas@legitmail.com', 'legitlucas', 'Portugal', './path/to/picture.png', true);
INSERT INTO users (id, username, email, password, country, picture, isadmin) VALUES(4, 'Rui Soares', 'rui@legitmail.com', 'legitrui', 'Portugal', './path/to/picture.png', true);
INSERT INTO users (id, username, email, password, country, picture, isAdmin) VALUES(5, '[redacted]', 'redac@legitmail.com', 'legitredac', 'Zimbabue', './path/to/default.png', false); --id 5 is deleted user
-------------------------------
-- Follows
-------------------------------
INSERT INTO follows (id1, id2) VALUES (1, 2);
INSERT INTO follows (id1, id2) VALUES (1, 3);
INSERT INTO follows (id1, id2) VALUES (1, 4);
INSERT INTO follows (id1, id2) VALUES (2, 1);
INSERT INTO follows (id1, id2) VALUES (2, 3);
INSERT INTO follows (id1, id2) VALUES (2, 4);
INSERT INTO follows (id1, id2) VALUES (3, 1);
INSERT INTO follows (id1, id2) VALUES (3, 2);
-------------------------------
-- Apply admin request
-------------------------------
INSERT INTO apply_admin_request(description, is_handled, id_user) VALUES ('I would like to be an admin to help manage news',false,1);
INSERT INTO apply_admin_request(description, is_handled, id_user) VALUES ('I would like to be an admin please!',true,2);
INSERT INTO apply_admin_request(description, is_handled, id_user) VALUES ('I would like to be an admin to manage reports',false,3);
INSERT INTO apply_admin_request(description, is_handled, id_user) VALUES ('I would like to be an admin to help manage ags',false,4);
-------------------------------
-- tag
-------------------------------
INSERT INTO tag(id, tag_name) VALUES (1, 'Gaming'); -- 1
INSERT INTO tag(id, tag_name) VALUES (2, 'Politics'); -- 2
INSERT INTO tag(id,tag_name) VALUES (3, 'Academia'); -- 3
INSERT INTO tag(id, tag_name) VALUES (4, 'Memes'); -- 4
INSERT INTO tag(id, tag_name) VALUES (5, 'Food'); -- 5
INSERT INTO tag(id, tag_name) VALUES (6, 'Animals'); -- 6
INSERT INTO tag(id, tag_name) VALUES (7, 'Celebrities'); -- 7
INSERT INTO tag(id, tag_name) VALUES (8, 'Movies'); -- 8
INSERT INTO tag(id, tag_name) VALUES (9, 'TV'); -- 9
INSERT INTO tag(id, tag_name) VALUES (10, 'Books'); -- 10
INSERT INTO tag(id, tag_name) VALUES (11, 'Technology'); -- 11
INSERT INTO tag(id, tag_name) VALUES (12, 'Hardware'); -- 12
INSERT INTO tag(id, tag_name) VALUES (13, 'Software'); -- 13
INSERT INTO tag(id, tag_name) VALUES (14, 'Sci-Fi'); -- 14
INSERT INTO tag(id, tag_name) VALUES (15, 'Fantasy'); -- 15
INSERT INTO tag(id, tag_name) VALUES (16, 'Sports'); -- 16
INSERT INTO tag(id, tag_name) VALUES (17, 'Photography'); -- 17
INSERT INTO tag(id, tag_name) VALUES (18, 'Science'); -- 18
INSERT INTO tag(id, tag_name) VALUES (19, 'DIY'); -- 19
INSERT INTO tag(id, tag_name) VALUES (20, 'Music'); -- 20
INSERT INTO tag(id, tag_name) VALUES (21, 'Anime'); -- 21
-------------------------------
-- News
-------------------------------
INSERT INTO news (id, title, content, date, picture, id_author) VALUES (1, 'Overwatch Fan Makes LEGO Bastion Figure for Their Brother', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Euismod lacinia at quis risus sed vulputate odio ut.
Dignissim convallis aenean et tortor. Eu feugiat pretium nibh ipsum consequat nisl. Interdum consectetur libero id faucibus.
Erat velit scelerisque in dictum non consectetur a.', '2022.10.20', './path/to/picture.png', 1);
INSERT INTO news (id, title, content, date, picture, id_author) VALUES (2, 'Here’s What to Expect from Season 3 of The Witcher', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Euismod lacinia at quis risus sed vulputate odio ut.
Dignissim convallis aenean et tortor. Eu feugiat pretium nibh ipsum consequat nisl. Interdum consectetur libero id faucibus.
Erat velit scelerisque in dictum non consectetur a.', '2022.10.20', './path/to/picture.png', 2);
INSERT INTO news (id, title, content, date, picture, id_author) VALUES (3, 'The State Of Destiny 2s Festival Of The Lost Is Unacceptable','Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Euismod lacinia at quis risus sed vulputate odio ut.
Dignissim convallis aenean et tortor. Eu feugiat pretium nibh ipsum consequat nisl. Interdum consectetur libero id faucibus.
Erat velit scelerisque in dictum non consectetur a.', '2022.10.20', './path/to/picture.png', 3);
INSERT INTO news (id, title, content, date, picture, id_author) VALUES (4, 'Bleach TYBW shocks fans with brutal character deaths in episode 2', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Euismod lacinia at quis risus sed vulputate odio ut.
Dignissim convallis aenean et tortor. Eu feugiat pretium nibh ipsum consequat nisl. Interdum consectetur libero id faucibus.
Erat velit scelerisque in dictum non consectetur a.', '2022.10.20', './path/to/picture.png', 4);
-------------------------------
-- news_favorite
-------------------------------
INSERT INTO news_favorite(id_user, id_news) VALUES (1, 1);
INSERT INTO news_favorite(id_user, id_news) VALUES (2, 2);
INSERT INTO news_favorite(id_user, id_news) VALUES (3, 4);
INSERT INTO news_favorite(id_user, id_news) VALUES (4, 3);
-------------------------------
-- news_vote
-------------------------------
INSERT INTO news_vote(id_user, id_news, is_liked) VALUES (1, 1, true);
INSERT INTO news_vote(id_user, id_news, is_liked) VALUES (1, 2, false);
INSERT INTO news_vote(id_user, id_news, is_liked) VALUES (2, 2, true);
INSERT INTO news_vote(id_user, id_news, is_liked) VALUES (2, 3, false);
INSERT INTO news_vote(id_user, id_news, is_liked) VALUES (3, 1, true);
INSERT INTO news_vote(id_user, id_news, is_liked) VALUES (3, 4, false);
-------------------------------
-- news_tag
-------------------------------
INSERT INTO news_tag (id_news, id_tag) VALUES (1, 1); -- gaming
INSERT INTO news_tag (id_news, id_tag) VALUES (2, 9); -- TV
INSERT INTO news_tag (id_news, id_tag) VALUES (2, 7); -- Celebrities
INSERT INTO news_tag (id_news, id_tag) VALUES (2, 1); -- Gaming
INSERT INTO news_tag (id_news, id_tag) VALUES (3, 1); -- Gaming
INSERT INTO news_tag (id_news, id_tag) VALUES (4, 8); -- Movies
INSERT INTO news_tag (id_news, id_tag) VALUES (4, 9); -- TV
INSERT INTO news_tag (id_news, id_tag) VALUES (4, 21); -- Anime
-------------------------------
-- comment
-------------------------------
INSERT INTO comment(id, content, id_news, id_comment, id_author) VALUES (1, 'Fake news!', 1, NULL, 1);
INSERT INTO comment(id, content, id_news, id_comment, id_author) VALUES (2, 'Very informative', 2, NULL, 2);
INSERT INTO comment(id, content, id_news, id_comment, id_author) VALUES (3, 'Loved it!', 2, 1, 3);
INSERT INTO comment(id, content, id_news, id_comment, id_author) VALUES (4, 'Source?', 3, 2, 4);
-------------------------------
-- comment_vote
-------------------------------
INSERT INTO comment_vote(id_user, id_comment, is_liked) VALUES (1, 1, true);
INSERT INTO comment_vote(id_user, id_comment, is_liked) VALUES (1, 2, false);
INSERT INTO comment_vote(id_user, id_comment, is_liked) VALUES (1, 3, true);
INSERT INTO comment_vote(id_user, id_comment, is_liked) VALUES (2, 2, true);
INSERT INTO comment_vote(id_user, id_comment, is_liked) VALUES (3, 1, false);
INSERT INTO comment_vote(id_user, id_comment, is_liked) VALUES (4, 1, true);
-------------------------------
-- tag_follow
-------------------------------
INSERT INTO tag_follow(id_user, id_tag) VALUES (1,1);
INSERT INTO tag_follow(id_user, id_tag) VALUES (2,2);
INSERT INTO tag_follow(id_user, id_tag) VALUES (3,3);
INSERT INTO tag_follow(id_user, id_tag) VALUES (4,4);
INSERT INTO tag_follow(id_user, id_tag) VALUES (1,4);
INSERT INTO tag_follow(id_user, id_tag) VALUES (2,5);
-------------------------------
-- tag_proposal
-------------------------------
INSERT INTO tag_proposal(tag_name, description, is_handled) VALUES ('Wholesome','I want to tag my cat pictures',false);
INSERT INTO tag_proposal(tag_name, description, is_handled) VALUES ('Mystery','I want to tag some books with this tag',false);
INSERT INTO tag_proposal(tag_name, description, is_handled) VALUES ('Manga','I want to tag my favorite manga without using the "books" tag',false);
INSERT INTO tag_proposal(tag_name, description, is_handled) VALUES ('Cars','This important tag is missing',false);
INSERT INTO tag_proposal(tag_name, description, is_handled) VALUES ('Anime','I want to tag my favorite anime shows without using the "TV" tag', true);
---------------------INSERT INTO report(report_type, report_text, is_handled, id_author, id_user, id_news, id_comment) VALUES ('UserReport','User insulted me', false,1,1,NULL, NULL);
----------
-- tag_proposal_user
-------------------------------
INSERT INTO tag_proposal_user(id_user, id_tag) VALUES (1, 1);
INSERT INTO tag_proposal_user(id_user, id_tag) VALUES (2, 1);
INSERT INTO tag_proposal_user(id_user, id_tag) VALUES (3, 1);
INSERT INTO tag_proposal_user(id_user, id_tag) VALUES (1, 2);
INSERT INTO tag_proposal_user(id_user, id_tag) VALUES (2, 2);
INSERT INTO tag_proposal_user(id_user, id_tag) VALUES (3, 3);
INSERT INTO tag_proposal_user(id_user, id_tag) VALUES (4, 3);
-------------------------------
-- report --
--'UserReport', 'NewsReport', 'CommentReport'
-------------------------------
INSERT INTO report(report_type, report_text, is_handled, id_author, id_user, id_news, id_comment) VALUES ('UserReport','User insulted me', false,1,1,NULL, NULL);
INSERT INTO report(report_type, report_text, is_handled, id_author, id_user, id_news, id_comment) VALUES ('NewsReport','Wrong use of tags', true,2,NULL,2, NULL);
INSERT INTO report(report_type, report_text, is_handled, id_author, id_user, id_news, id_comment) VALUES ('CommentReport','Offensive comment', false,3,NULL,NULL,1);
INSERT INTO report(report_type, report_text, is_handled, id_author, id_user, id_news, id_comment) VALUES ('CommentReport','Spam', false,4, NULL, NULL, 2);
-------------------------------
-- notification
-- 'NewsVote', 'CommentVote', 'NewsComment'
-------------------------------
INSERT INTO notification(notification_type, is_viewed, id_user, id_news, id_comment) VALUES ('NewsVote', false, 1, 1, NULL);
INSERT INTO notification(notification_type, is_viewed, id_user, id_news, id_comment) VALUES ('CommentVote', false, 2, NULL, 3);
INSERT INTO notification(notification_type, date, is_viewed, id_user, id_news, id_comment) VALUES ('NewsComment', '2022.10.20', true, 2, NULL, 1);
INSERT INTO notification(notification_type, date, is_viewed, id_user, id_news, id_comment) VALUES ('NewsVote', '2022.10.20', true, 4, 4, NULL);
```
---
## Revision history
- A4: Conceptual Data Model
- A5: Relational Schema, validation and schema refinement
- A6: Indexes, triggers, transactions and database population
***
* André Morais, up202005303@edu.fe.up.pt (editor)
* João Teixeira, up202005437@edu.fe.up.pt
* Lucas Sousa, up202004682@edu.fe.up.pt
* Rui Soares, up202103631@edu.fe.up.pt
lbaw2223-t8g6, 29/10/22