| | 1 | = Advanced Database Development (SQL DDL) |
| | 2 | |
| | 3 | == Custom Domains |
| | 4 | {{{ |
| | 5 | -- Check email format |
| | 6 | CREATE DOMAIN valid_email AS VARCHAR(255) |
| | 7 | CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'); |
| | 8 | |
| | 9 | -- Check rating range from 0 to 5 |
| | 10 | CREATE DOMAIN story_rating AS DECIMAL(3,2) |
| | 11 | CHECK (VALUE >= 0.00 AND VALUE <= 5.00); |
| | 12 | |
| | 13 | -- Status of accepted values |
| | 14 | CREATE DOMAIN story_status AS VARCHAR(50) |
| | 15 | CHECK (VALUE IN ('draft', 'published', 'archived')); |
| | 16 | |
| | 17 | -- Permission level from 1 to 5 |
| | 18 | CREATE DOMAIN permission_lvl AS INTEGER |
| | 19 | CHECK (VALUE >= 1 AND VALUE <= 5); |
| | 20 | |
| | 21 | -- URL / image path |
| | 22 | CREATE DOMAIN valid_url AS VARCHAR(2048) |
| | 23 | CHECK (VALUE ~* '^https?://.+'); |
| | 24 | }}} |
| | 25 | |
| | 26 | == Views |
| | 27 | {{{ |
| | 28 | -- Detailed view for each story with statistic |
| | 29 | CREATE OR REPLACE VIEW story_details_view AS |
| | 30 | SELECT |
| | 31 | s.story_id, |
| | 32 | s.short_description, |
| | 33 | s.mature_content, |
| | 34 | s.image, |
| | 35 | s.story_created_at, |
| | 36 | u.username AS writer, |
| | 37 | u.user_name, |
| | 38 | u.surname, |
| | 39 | st.status, |
| | 40 | COUNT(DISTINCT ch.chapter_id) AS total_chapters, |
| | 41 | COALESCE(SUM(ch.view_count), 0) AS total_views, |
| | 42 | ROUND(AVG(ch.rating), 2) AS avg_rating, |
| | 43 | COUNT(DISTINCT l.user_id) AS total_likes, |
| | 44 | COUNT(DISTINCT c.comment_id) AS total_comments, |
| | 45 | COUNT(DISTINCT hg.genre_id) AS total_genres, |
| | 46 | COUNT(DISTINCT col.user_id) AS total_collaborators |
| | 47 | FROM story s |
| | 48 | JOIN writer w ON s.user_id = w.user_id |
| | 49 | JOIN users u ON w.user_id = u.user_id |
| | 50 | JOIN "status" st ON s.story_id = st.story_id |
| | 51 | LEFT JOIN chapter ch ON s.story_id = ch.story_id |
| | 52 | LEFT JOIN likes l ON s.story_id = l.story_id |
| | 53 | LEFT JOIN "comment" c ON s.story_id = c.story_id |
| | 54 | LEFT JOIN has_genre hg ON s.story_id = hg.story_id |
| | 55 | LEFT JOIN collaboration col ON s.story_id = col.story_id |
| | 56 | GROUP BY |
| | 57 | s.story_id, s.short_description, s.mature_content, |
| | 58 | s.image, s.story_created_at, |
| | 59 | u.username, u.user_name, u.surname, st.status; |
| | 60 | -- Top 10 most popular stories by likes count = |
| | 61 | CREATE MATERIALIZED VIEW top_stories_by_likes AS |
| | 62 | SELECT |
| | 63 | s.story_id, |
| | 64 | s.short_description, |
| | 65 | u.username AS writer, |
| | 66 | st.status, |
| | 67 | COUNT(DISTINCT l.user_id) AS total_likes, |
| | 68 | COUNT(DISTINCT c.comment_id) AS total_comments, |
| | 69 | ROUND(AVG(ch.rating), 2) AS avg_rating |
| | 70 | FROM story s |
| | 71 | JOIN writer w ON s.user_id = w.user_id |
| | 72 | JOIN users u ON w.user_id = u.user_id |
| | 73 | JOIN "status" st ON s.story_id = st.story_id |
| | 74 | AND st.status = 'published' |
| | 75 | LEFT JOIN likes l ON s.story_id = l.story_id |
| | 76 | LEFT JOIN "comment" c ON s.story_id = c.story_id |
| | 77 | LEFT JOIN chapter ch ON s.story_id = ch.story_id |
| | 78 | GROUP BY |
| | 79 | s.story_id, s.short_description, |
| | 80 | u.username, st.status |
| | 81 | ORDER BY total_likes DESC |
| | 82 | LIMIT 10; |
| | 83 | |
| | 84 | -- View for user profile with all his stories |
| | 85 | CREATE OR REPLACE VIEW writer_profile_view AS |
| | 86 | SELECT |
| | 87 | u.user_id, |
| | 88 | u.username, |
| | 89 | u.user_name, |
| | 90 | u.surname, |
| | 91 | u.user_created_at, |
| | 92 | COUNT(DISTINCT s.story_id)AS total_stories, |
| | 93 | COUNT(DISTINCT ch.chapter_id) AS total_chapters, |
| | 94 | COALESCE(SUM(ch.view_count), 0) AS total_views, |
| | 95 | COUNT(DISTINCT l.user_id) AS total_likes, |
| | 96 | COUNT(DISTINCT c.comment_id) AS total_comments, |
| | 97 | ROUND(AVG(ch.rating), 2) AS avg_rating |
| | 98 | FROM users u |
| | 99 | JOIN writer w ON u.user_id = w.user_id |
| | 100 | LEFT JOIN story s ON w.user_id = s.user_id |
| | 101 | LEFT JOIN chapter ch ON s.story_id = ch.story_id |
| | 102 | LEFT JOIN likes l ON s.story_id = l.story_id |
| | 103 | LEFT JOIN comment c ON s.story_id = c.story_id |
| | 104 | GROUP BY |
| | 105 | u.user_id, u.username, u.user_name, |
| | 106 | u.surname, u.user_created_at; |
| | 107 | }}} |
| | 108 | |
| | 109 | == Stored Procedures / Functions |
| | 110 | -- Table for daily top story |
| | 111 | CREATE TABLE daily_top_stories ( |
| | 112 | day DATE PRIMARY KEY, |
| | 113 | story_id INTEGER NULL REFERENCES story(story_id), |
| | 114 | total_likes BIGINT |
| | 115 | ); |
| | 116 | |
| | 117 | -- Procedure: saves the most liked story of the previous day |
| | 118 | CREATE OR REPLACE PROCEDURE get_daily_top_story() |
| | 119 | LANGUAGE plpgsql |
| | 120 | AS $$ |
| | 121 | DECLARE |
| | 122 | yesterday TIMESTAMP; |
| | 123 | BEGIN |
| | 124 | yesterday := CURRENT_DATE - 1; |
| | 125 | |
| | 126 | IF EXISTS ( |
| | 127 | SELECT 1 FROM likes |
| | 128 | WHERE like_created_at BETWEEN yesterday AND CURRENT_DATE |
| | 129 | ) THEN |
| | 130 | INSERT INTO daily_top_stories (day, story_id, total_likes) |
| | 131 | SELECT |
| | 132 | CURRENT_DATE - 1, |
| | 133 | s.story_id, |
| | 134 | COUNT(l.user_id) AS total_likes |
| | 135 | FROM story s |
| | 136 | JOIN likes l ON s.story_id = l.story_id |
| | 137 | WHERE l.like_created_at BETWEEN yesterday AND CURRENT_DATE |
| | 138 | GROUP BY s.story_id |
| | 139 | ORDER BY total_likes DESC |
| | 140 | LIMIT 1; |
| | 141 | ELSE |
| | 142 | INSERT INTO daily_top_stories (day, story_id, total_likes) |
| | 143 | VALUES (CURRENT_DATE - 1, NULL, 0); |
| | 144 | END IF; |
| | 145 | END; |
| | 146 | $$; |
| | 147 | |
| | 148 | -- Procedure: sends a notification to all collaborators of a story |
| | 149 | CREATE OR REPLACE PROCEDURE notify_collaborators( |
| | 150 | p_story_id INTEGER, |
| | 151 | p_message TEXT, |
| | 152 | p_type VARCHAR(50), |
| | 153 | p_link VARCHAR(500) |
| | 154 | ) |
| | 155 | LANGUAGE plpgsql |
| | 156 | AS $$ |
| | 157 | DECLARE |
| | 158 | collab RECORD; |
| | 159 | BEGIN |
| | 160 | FOR collab IN |
| | 161 | SELECT c.user_id |
| | 162 | FROM collaboration c |
| | 163 | WHERE c.story_id = p_story_id |
| | 164 | LOOP |
| | 165 | INSERT INTO notification ( |
| | 166 | notification_content, |
| | 167 | is_read, |
| | 168 | recipient_user_id, |
| | 169 | type, |
| | 170 | link, |
| | 171 | notification_created_at |
| | 172 | ) |
| | 173 | VALUES ( |
| | 174 | p_message, |
| | 175 | FALSE, |
| | 176 | collab.user_id, |
| | 177 | p_type, |
| | 178 | p_link, |
| | 179 | CURRENT_TIMESTAMP |
| | 180 | ); |
| | 181 | END LOOP; |
| | 182 | END; |
| | 183 | $$; |
| | 184 | |
| | 185 | -- Function: calculates the engagement score for a given story |
| | 186 | CREATE OR REPLACE FUNCTION calculate_engagement_score(p_story_id INTEGER) |
| | 187 | RETURNS DECIMAL |
| | 188 | LANGUAGE sql |
| | 189 | AS $$ |
| | 190 | SELECT ROUND( |
| | 191 | (COUNT(DISTINCT l.user_id) + COUNT(DISTINCT c.comment_id))::DECIMAL |
| | 192 | / NULLIF(COALESCE(SUM(ch.view_count), 0), 0) * 100, 2 |
| | 193 | ) |
| | 194 | FROM story s |
| | 195 | LEFT JOIN likes l ON s.story_id = l.story_id |
| | 196 | LEFT JOIN "comment" c ON s.story_id = c.story_id |
| | 197 | LEFT JOIN chapter ch ON s.story_id = ch.story_id |
| | 198 | WHERE s.story_id = p_story_id; |
| | 199 | $$; |