Changes between Version 6 and Version 7 of P6


Ignore:
Timestamp:
04/25/26 22:04:02 (7 days ago)
Author:
211099
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • P6

    v6 v7  
    105105==== SQL
    106106{{{
    107 WITH
    108 published_story AS (
    109     SELECT s.story_id, s.short_description
    110     FROM story s
    111     JOIN status st ON s.story_id = st.story_id
    112     WHERE s.story_id = 1
    113       AND st.status = 'published'
     107WITH genre_annual AS (
     108    SELECT
     109        DATE_TRUNC('year', s.story_created_at) AS year,
     110        g.genre_id,
     111        g.genre_name,
     112        COUNT(DISTINCT s.story_id) AS total_stories,
     113        COUNT(DISTINCT w.user_id) AS total_writers,
     114        COALESCE(SUM(ch.view_count), 0) AS total_views,
     115        COALESCE(SUM(ch.word_count), 0) AS total_words,
     116        COUNT(DISTINCT l.user_id) AS total_likes,
     117        COUNT(DISTINCT c.comment_id) AS total_comments,
     118        ROUND(AVG(ch.rating), 2) AS avg_rating
     119    FROM genre               g
     120    JOIN has_genre           hg ON g.genre_id     = hg.genre_id
     121    JOIN story               s  ON hg.story_id    = s.story_id
     122    JOIN writer              w  ON s.user_id       = w.user_id
     123    JOIN status              st ON s.story_id      = st.story_id
     124                                AND st.status      = 'published'
     125    LEFT JOIN chapter        ch ON s.story_id      = ch.story_id
     126    LEFT JOIN likes          l  ON s.story_id      = l.story_id
     127    LEFT JOIN comment        c  ON s.story_id      = c.story_id
     128    GROUP BY
     129        DATE_TRUNC('year', s.story_created_at),
     130        g.genre_id, g.genre_name
    114131),
    115 user_list AS (
    116     SELECT list_id, list_name, is_public
    117     FROM reading_list
    118     WHERE list_id = 1
    119       AND user_id = 4
    120 ),
    121 already_added AS (
    122     SELECT 1
    123     FROM reading_list_items
    124     WHERE list_id = 1
    125       AND story_id = 1
    126 ),
    127 list_items AS (
    128     SELECT
    129         rli.list_id,
    130         COUNT(rli.story_id) AS total_items,
    131         MAX(rli.added_at)   AS last_added_at
    132     FROM reading_list_items rli
    133     WHERE rli.list_id = 1
    134     GROUP BY rli.list_id
     132with_metrics AS (
     133    SELECT
     134        *,
     135        ROUND(
     136            (total_likes + total_comments)::DECIMAL
     137            / NULLIF(total_views, 0) * 100, 2
     138        ) AS engagement_rate,
     139        ROUND(
     140            total_views::DECIMAL
     141            / NULLIF(total_stories, 0), 2
     142        ) AS avg_views_per_story,
     143        LAG(total_views) OVER (
     144            PARTITION BY genre_id ORDER BY year
     145        ) AS prev_year_views,
     146        LAG(total_stories) OVER (
     147            PARTITION BY genre_id ORDER BY year
     148        ) AS prev_year_stories
     149    FROM genre_annual
    135150)
    136151SELECT
    137     rl.list_id,
    138     rl.list_name,
    139     rl.is_public,
    140     u.username                              AS owner,
    141     ps.story_id                             AS story_to_add,
    142     ps.short_description                    AS story_description,
    143     COALESCE(li.total_items, 0)             AS total_items,
    144     li.last_added_at,
    145     CASE
    146         WHEN NOT EXISTS (SELECT 1 FROM published_story) THEN 'Failed: story not published or not found'
    147         WHEN NOT EXISTS (SELECT 1 FROM user_list)       THEN 'Failed: reading list not found or not owned by you'
    148         WHEN EXISTS     (SELECT 1 FROM already_added)   THEN 'Story already in list'
    149         ELSE                                                 'Ready to add story'
    150     END                                     AS status
    151 FROM reading_list               rl
    152 JOIN users                      u   ON rl.user_id  = u.user_id
    153 LEFT JOIN published_story       ps  ON ps.story_id = 1
    154 LEFT JOIN list_items            li  ON li.list_id  = rl.list_id
    155 WHERE rl.list_id = 1;
     152    TO_CHAR(year, 'YYYY') AS year,
     153    genre_name,
     154    total_stories,
     155    total_writers,
     156    total_views,
     157    avg_views_per_story,
     158    total_likes,
     159    total_comments,
     160    COALESCE(avg_rating, 0) AS avg_rating,
     161    COALESCE(engagement_rate, 0) AS engagement_rate,
     162    ROUND(
     163        (total_views - prev_year_views)::DECIMAL
     164        / NULLIF(prev_year_views, 0) * 100, 2
     165    ) AS yoy_views_growth_pct,
     166    ROUND(
     167        (total_stories - prev_year_stories)::DECIMAL
     168        / NULLIF(prev_year_stories, 0) * 100, 2
     169    ) AS yoy_stories_growth_pct,
     170    RANK() OVER (
     171        PARTITION BY year
     172        ORDER BY total_views DESC
     173    ) AS popularity_rank,
     174    RANK() OVER (
     175        PARTITION BY year
     176        ORDER BY engagement_rate DESC
     177    ) AS engagement_rank
     178FROM with_metrics
     179ORDER BY year DESC, popularity_rank;
    156180}}}
    157181
    158182=== Relational Algebra
    159183{{{
    160 ValidateAndInsertStoryIntoUserReadingListWithConfirmation
    161 
    162 
    163 PublishedStory ←
    164 π story_id, short_description
    165 (
    166   σ s.story_id = 1 ∧ st.status = 'published'
    167   (
    168     story s
    169     ⨝ (s.story_id = st.story_id) status st
    170   )
    171 )
    172 
    173 UserList ←
    174 π list_id, list_name, is_public
    175 (
    176   σ list_id = 1 ∧ user_id = 4
    177   (
    178     reading_list
    179   )
    180 )
    181 
    182 AlreadyAdded ←
    183 σ list_id = 1 ∧ story_id = 1
    184 (
    185   reading_list_items
    186 )
    187 
    188 ListItems ←
    189 γ list_id;
    190   total_items  := COUNT(story_id),
    191   last_added_at := MAX(added_at)
    192 (
    193   σ list_id = 1
    194   (
    195     reading_list_items
    196   )
     184Annual Genre Popularity and Engagement Trend
     185
     186
     187PublishedStories ←
     188σ st.status = 'published'
     189(
     190  genre g
     191  ⨝ (g.genre_id = hg.genre_id)  has_genre hg
     192  ⨝ (hg.story_id = s.story_id)  story s
     193  ⨝ (s.user_id = w.user_id)     writer w
     194  ⨝ (s.story_id = st.story_id)  status st
     195)
     196
     197WithChapters ←
     198PublishedStories
     199⟕ (s.story_id = ch.story_id) chapter ch
     200
     201WithLikes ←
     202WithChapters
     203⟕ (s.story_id = l.story_id) likes l
     204
     205WithComments ←
     206WithLikes
     207⟕ (s.story_id = c.story_id) comment c
     208
     209GenreAnnual ←
     210γ
     211  year       := DATE_TRUNC('year', s.story_created_at),
     212  genre_id   := g.genre_id,
     213  genre_name := g.genre_name;
     214  total_stories  := COUNT(DISTINCT s.story_id),
     215  total_writers  := COUNT(DISTINCT w.user_id),
     216  total_views    := COALESCE(SUM(ch.view_count), 0),
     217  total_words    := COALESCE(SUM(ch.word_count), 0),
     218  total_likes    := COUNT(DISTINCT l.user_id),
     219  total_comments := COUNT(DISTINCT c.comment_id),
     220  avg_rating     := ROUND(AVG(ch.rating), 2)
     221(
     222  WithComments
     223)
     224
     225WithMetrics ←
     226π
     227  year,
     228  genre_id,
     229  genre_name,
     230  total_stories,
     231  total_writers,
     232  total_views,
     233  total_words,
     234  total_likes,
     235  total_comments,
     236  avg_rating,
     237  ROUND((total_likes + total_comments) / NULLIF(total_views, 0) * 100, 2)
     238                                          → engagement_rate,
     239  ROUND(total_views / NULLIF(total_stories, 0), 2)
     240                                          → avg_views_per_story,
     241  LAG(total_views)   OVER (PARTITION BY genre_id ORDER BY year)
     242                                          → prev_year_views,
     243  LAG(total_stories) OVER (PARTITION BY genre_id ORDER BY year)
     244                                          → prev_year_stories
     245(
     246  GenreAnnual
    197247)
    198248
    199249Result ←
    200250π
    201   rl.list_id,
    202   rl.list_name,
    203   rl.is_public,
    204   u.username                                        → owner,
    205   ps.story_id                                       → story_to_add,
    206   ps.short_description                              → story_description,
    207   COALESCE(li.total_items, 0)                       → total_items,
    208   li.last_added_at,
    209   CASE
    210     WHEN PublishedStory = ∅   → 'Failed: story not published or not found'
    211     WHEN UserList = ∅         → 'Failed: reading list not found or not owned by you'
    212     WHEN AlreadyAdded ≠ ∅     → 'Story already in list'
    213     ELSE                      → 'Ready to add story'
    214   END                                               → status
    215 (
    216   (
    217     (
    218       σ rl.list_id = 1 (reading_list rl)
    219       ⨝ (rl.user_id = u.user_id) users u
    220     )
    221     ⟕ (ps.story_id = 1) PublishedStory ps
    222   )
    223   ⟕ (li.list_id = rl.list_id) ListItems li
     251  TO_CHAR(year, 'YYYY')                   → year,
     252  genre_name,
     253  total_stories,
     254  total_writers,
     255  total_views,
     256  avg_views_per_story,
     257  total_likes,
     258  total_comments,
     259  COALESCE(avg_rating, 0)                 → avg_rating,
     260  COALESCE(engagement_rate, 0)            → engagement_rate,
     261  ROUND(
     262    (total_views - prev_year_views)
     263    / NULLIF(prev_year_views, 0) * 100, 2
     264  )                                       → yoy_views_growth_pct,
     265  ROUND(
     266    (total_stories - prev_year_stories)
     267    / NULLIF(prev_year_stories, 0) * 100, 2
     268  )                                       → yoy_stories_growth_pct,
     269  RANK() OVER (PARTITION BY year ORDER BY total_views DESC)
     270                                          → popularity_rank,
     271  RANK() OVER (PARTITION BY year ORDER BY engagement_rate DESC)
     272                                          → engagement_rank
     273(
     274  WithMetrics
    224275)
    225276}}}
     
    228279==== SQL
    229280{{{
    230 WITH
    231 owned_story AS (
    232     SELECT s.story_id, s.short_description
    233     FROM story s
    234     JOIN writer w ON s.user_id = w.user_id
    235     WHERE s.story_id = 5
    236       AND s.user_id = :owner_user_id
    237 ),
    238 invited_writer AS (
    239     SELECT u.user_id, u.username, u.user_name, u.surname
    240     FROM users u
    241     JOIN writer w ON u.user_id = w.user_id
    242     WHERE u.username = :invited_username
    243 ),
    244 already_collaborating AS (
    245     SELECT 1
    246     FROM collaboration
    247     WHERE user_id  = (SELECT user_id  FROM invited_writer)
    248       AND story_id = (SELECT story_id FROM owned_story)
    249 ),
    250 current_collaborators AS (
    251     SELECT
     281WITH quarterly_stats AS (
     282    SELECT
     283        DATE_TRUNC('quarter', s.story_created_at)   AS quarter,
     284        u.user_id,
    252285        u.username,
    253286        u.user_name,
    254287        u.surname,
    255         r.roles,
    256         p.permission_level,
    257         c.collab_created_at
    258     FROM collaboration    c
    259     JOIN users            u ON c.user_id  = u.user_id
    260     JOIN roles            r ON c.user_id  = r.user_id AND c.story_id = r.story_id
    261     JOIN permission_level p ON c.user_id  = p.user_id AND c.story_id = p.story_id
    262     WHERE c.story_id = (SELECT story_id FROM owned_story)
     288        COUNT(DISTINCT s.story_id) AS stories_published,
     289        COUNT(DISTINCT ch.chapter_id) AS chapters_written,
     290        COALESCE(SUM(ch.view_count), 0) AS total_views,
     291        COALESCE(SUM(ch.word_count), 0) AS total_words,
     292        COUNT(DISTINCT l.user_id) AS total_likes,
     293        COUNT(DISTINCT c.comment_id) AS total_comments,
     294        ROUND(AVG(ch.rating), 2) AS avg_rating
     295    FROM story               s
     296    JOIN writer              w  ON s.user_id    = w.user_id
     297    JOIN users               u  ON w.user_id    = u.user_id
     298    JOIN status              st ON s.story_id   = st.story_id
     299                                AND st.status   = 'published'
     300    LEFT JOIN chapter        ch ON s.story_id   = ch.story_id
     301    LEFT JOIN likes          l  ON s.story_id   = l.story_id
     302    LEFT JOIN comment        c  ON s.story_id   = c.story_id
     303    GROUP BY
     304        DATE_TRUNC('quarter', s.story_created_at),
     305        u.user_id, u.username, u.user_name, u.surname
    263306),
    264 notification_preview AS (
    265     SELECT
    266         iw.user_id AS recipient_user_id,
    267         iw.username AS recipient_username,
    268         'You have been invited to collaborate on "'
    269             || os.short_description || '"' AS notification_content,
    270         'collaboration' AS type,
    271         '/story/' || os.story_id::VARCHAR   AS link
    272     FROM invited_writer iw
    273     CROSS JOIN owned_story os
     307with_growth AS (
     308    SELECT
     309        *,
     310        LAG(total_views)    OVER (PARTITION BY user_id ORDER BY quarter) AS prev_views,
     311        LAG(total_likes)    OVER (PARTITION BY user_id ORDER BY quarter) AS prev_likes,
     312        LAG(total_comments) OVER (PARTITION BY user_id ORDER BY quarter) AS prev_comments,
     313        ROUND(
     314            (total_views - LAG(total_views) OVER (PARTITION BY user_id ORDER BY quarter))
     315            ::DECIMAL
     316            / NULLIF(LAG(total_views) OVER (PARTITION BY user_id ORDER BY quarter), 0)
     317            * 100, 2
     318        ) AS views_growth_pct,
     319        ROUND(
     320            (total_likes - LAG(total_likes) OVER (PARTITION BY user_id ORDER BY quarter))
     321            ::DECIMAL
     322            / NULLIF(LAG(total_likes) OVER (PARTITION BY user_id ORDER BY quarter), 0)
     323            * 100, 2
     324        ) AS likes_growth_pct
     325    FROM quarterly_stats
    274326)
    275327SELECT
    276     cc.username,
    277     cc.user_name,
    278     cc.surname,
    279     cc.roles,
    280     cc.permission_level,
    281     cc.collab_created_at,
    282     COUNT(*) OVER () AS total_collaborators,
    283     np.notification_content AS pending_notification,
    284     CASE
    285         WHEN NOT EXISTS (SELECT 1 FROM owned_story)          THEN 'Failed: story not found or not owned by you'
    286         WHEN NOT EXISTS (SELECT 1 FROM invited_writer)       THEN 'Failed: invited user is not a writer'
    287         WHEN EXISTS     (SELECT 1 FROM already_collaborating) THEN 'User is already a collaborator'
    288         ELSE                                                       'Ready to add collaborator'
    289     END   AS status
    290 FROM current_collaborators          cc
    291 LEFT JOIN notification_preview      np ON np.recipient_username = :invited_username
    292 ORDER BY cc.collab_created_at;
     328    TO_CHAR(quarter, 'YYYY "Q"Q') AS period,
     329    username,
     330    user_name,
     331    surname,
     332    stories_published,
     333    chapters_written,
     334    total_words,
     335    total_views,
     336    COALESCE(views_growth_pct, 0) AS views_growth_pct,
     337    total_likes,
     338    COALESCE(likes_growth_pct, 0) AS likes_growth_pct,
     339    total_comments,
     340    COALESCE(avg_rating, 0) AS avg_rating,
     341    RANK() OVER (
     342        PARTITION BY quarter
     343        ORDER BY total_views DESC
     344    ) AS rank_by_views
     345FROM with_growth
     346ORDER BY quarter DESC, rank_by_views;
    293347}}}
    294348=== Relational Algebra
    295349{{{
    296 OwnedStory ←
    297 π story_id, short_description
    298 (
    299   σ s.story_id = :story_id ∧ s.user_id = :owner_user_id
    300   (
    301     story s
    302     ⨝ (s.user_id = w.user_id) writer w
    303   )
    304 )
    305 
    306 InvitedWriter ←
    307 π user_id, username, user_name, surname
    308 (
    309   σ u.username = :invited_username
    310   (
    311     users u
    312     ⨝ (u.user_id = w.user_id) writer w
    313   )
    314 )
    315 
    316 AlreadyCollaborating ←
    317 σ user_id = (π user_id (InvitedWriter))
    318   ∧ story_id = (π story_id (OwnedStory))
    319 (
    320   collaboration
    321 )
    322 
    323 CurrentCollaborators ←
     350PublishedBase ←
     351σ st.status = 'published'
     352(
     353  story s
     354  ⨝ (s.user_id = w.user_id)   writer w
     355  ⨝ (w.user_id = u.user_id)   users u
     356  ⨝ (s.story_id = st.story_id) status st
     357)
     358
     359WithChapters ←
     360PublishedBase
     361⟕ (s.story_id = ch.story_id) chapter ch
     362
     363WithLikes ←
     364WithChapters
     365⟕ (s.story_id = l.story_id) likes l
     366
     367WithComments ←
     368WithLikes
     369⟕ (s.story_id = c.story_id) comment c
     370
     371QuarterlyStats ←
     372γ
     373  quarter    := DATE_TRUNC('quarter', s.story_created_at),
     374  user_id    := u.user_id,
     375  username   := u.username,
     376  user_name  := u.user_name,
     377  surname    := u.surname;
     378  stories_published := COUNT(DISTINCT s.story_id),
     379  chapters_written  := COUNT(DISTINCT ch.chapter_id),
     380  total_views       := COALESCE(SUM(ch.view_count), 0),
     381  total_words       := COALESCE(SUM(ch.word_count), 0),
     382  total_likes       := COUNT(DISTINCT l.user_id),
     383  total_comments    := COUNT(DISTINCT c.comment_id),
     384  avg_rating        := ROUND(AVG(ch.rating), 2)
     385(
     386  WithComments
     387)
     388
     389WithGrowth ←
    324390π
    325   u.username, u.user_name, u.surname,
    326   r.roles, p.permission_level,
    327   c.collab_created_at
    328 (
    329   (
    330     (
    331       σ story_id = (π story_id (OwnedStory)) (collaboration c)
    332       ⨝ (c.user_id = u.user_id) users u
    333     )
    334     ⨝ (c.user_id = r.user_id ∧ c.story_id = r.story_id) roles r
    335   )
    336   ⨝ (c.user_id = p.user_id ∧ c.story_id = p.story_id) permission_level p
    337 )
    338 
    339 NotificationPreview ←
    340 π
    341   iw.user_id        → recipient_user_id,
    342   iw.username       → recipient_username,
    343   'You have been invited to collaborate on "' || os.short_description || '"'
    344                     → notification_content,
    345   'collaboration'   → type,
    346   '/story/' || os.story_id → link
    347 (
    348   InvitedWriter iw
    349   ⨯ OwnedStory os
     391  quarter,
     392  user_id,
     393  username,
     394  user_name,
     395  surname,
     396  stories_published,
     397  chapters_written,
     398  total_views,
     399  total_words,
     400  total_likes,
     401  total_comments,
     402  avg_rating,
     403  LAG(total_views)    OVER (PARTITION BY user_id ORDER BY quarter)
     404                                            → prev_views,
     405  LAG(total_likes)    OVER (PARTITION BY user_id ORDER BY quarter)
     406                                            → prev_likes,
     407  LAG(total_comments) OVER (PARTITION BY user_id ORDER BY quarter)
     408                                            → prev_comments,
     409  ROUND(
     410    (total_views - LAG(total_views) OVER (PARTITION BY user_id ORDER BY quarter))
     411    / NULLIF(LAG(total_views) OVER (PARTITION BY user_id ORDER BY quarter), 0)
     412    * 100, 2
     413  )                                         → views_growth_pct,
     414  ROUND(
     415    (total_likes - LAG(total_likes) OVER (PARTITION BY user_id ORDER BY quarter))
     416    / NULLIF(LAG(total_likes) OVER (PARTITION BY user_id ORDER BY quarter), 0)
     417    * 100, 2
     418  )                                         → likes_growth_pct
     419(
     420  QuarterlyStats
    350421)
    351422
    352423Result ←
    353424π
    354   cc.username,
    355   cc.user_name,
    356   cc.surname,
    357   cc.roles,
    358   cc.permission_level,
    359   cc.collab_created_at,
    360   COUNT(*) OVER ()  → total_collaborators,
    361   np.notification_content → pending_notification,
    362   CASE
    363     WHEN OwnedStory = ∅           → 'Failed: story not found or not owned by you'
    364     WHEN InvitedWriter = ∅        → 'Failed: invited user is not a writer'
    365     WHEN AlreadyCollaborating ≠ ∅ → 'User is already a collaborator'
    366     ELSE                          → 'Ready to add collaborator'
    367   END               → status
    368 (
    369   CurrentCollaborators cc
    370   ⟕ (cc.username = np.recipient_username) NotificationPreview np
    371 )
    372 }}}
     425  TO_CHAR(quarter, 'YYYY "Q"Q')             → period,
     426  username,
     427  user_name,
     428  surname,
     429  stories_published,
     430  chapters_written,
     431  total_words,
     432  total_views,
     433  COALESCE(views_growth_pct, 0)             → views_growth_pct,
     434  total_likes,
     435  COALESCE(likes_growth_pct, 0)             → likes_growth_pct,
     436  total_comments,
     437  COALESCE(avg_rating, 0)                   → avg_rating,
     438  RANK() OVER (PARTITION BY quarter ORDER BY total_views DESC)
     439                                            → rank_by_views
     440(
     441  WithGrowth
     442)
     443}}}
     444}}}