Add controversial ranking (#3205)
authorPavlos Smith <57727226+iByteABit256@users.noreply.github.com>
Wed, 26 Jul 2023 17:07:05 +0000 (20:07 +0300)
committerGitHub <noreply@github.com>
Wed, 26 Jul 2023 17:07:05 +0000 (13:07 -0400)
* Added controversy rank property to posts and comments, and ability to sort by it

* Triggers instead of schedules tasks, integer -> double, TODO: comments don't seem to get updated with floats, divide SortTypes

* Created PersonSortType

* PersonSortType::MostComments case

* Removed unused PartialOrd trait

* Added new person sort type mappings

* SortType -> PersonSortType

* fixes

* cargo fmt

* fixes after merge with main

* Fixed bug in controversy rank trigger, removed TopX sorts from PersonSortType and added CommentScore instead

* Uncovered enum case

* clippy

* reset translation changes

* translations

* translations

* Added additional hot ordering on controversial posts and comments

* featured local and featured community added to controversy rank index, additional order_by removed (?), added post_score and post_count to PersonSortType

* Woodpecker rerun

* cargo fmt

* woodpecker rerun

* fixed controversy_rank order

* fix

* Readded migration as latest, removed second update statement for setting controversy rank

17 files changed:
crates/apub/src/api/search.rs
crates/db_schema/src/aggregates/structs.rs
crates/db_schema/src/lib.rs
crates/db_schema/src/schema.rs
crates/db_schema/src/utils.rs
crates/db_views/src/comment_report_view.rs
crates/db_views/src/comment_view.rs
crates/db_views/src/post_report_view.rs
crates/db_views/src/post_view.rs
crates/db_views/src/structs.rs
crates/db_views_actor/src/comment_reply_view.rs
crates/db_views_actor/src/community_view.rs
crates/db_views_actor/src/person_mention_view.rs
crates/db_views_actor/src/person_view.rs
crates/db_views_actor/src/structs.rs
migrations/2023-07-26-000217_create_controversial_indexes/down.sql [new file with mode: 0644]
migrations/2023-07-26-000217_create_controversial_indexes/up.sql [new file with mode: 0644]

index ca84606ffe1c3db5d9ece363978b7af91423aafb..aaea69e07944fd343988989a40749ea76a119854 100644 (file)
@@ -8,7 +8,7 @@ use lemmy_api_common::{
 };
 use lemmy_db_schema::{
   source::{community::Community, local_site::LocalSite},
-  utils::post_to_comment_sort_type,
+  utils::{post_to_comment_sort_type, post_to_person_sort_type},
   SearchType,
 };
 use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery};
@@ -98,7 +98,7 @@ pub async fn search(
     }
     SearchType::Users => {
       users = PersonQuery {
-        sort: (sort),
+        sort: (sort.map(post_to_person_sort_type)),
         search_term: (Some(q)),
         page: (page),
         limit: (limit),
@@ -168,7 +168,7 @@ pub async fn search(
         vec![]
       } else {
         PersonQuery {
-          sort: (sort),
+          sort: (sort.map(post_to_person_sort_type)),
           search_term: (Some(q)),
           page: (page),
           limit: (limit),
index 1af94a800a76d66f0e0e024c25caa6aa19907c19..3b3612bb78188605a80af75dc1af348370114b93 100644 (file)
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
 #[cfg(feature = "full")]
 use ts_rs::TS;
 
-#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
+#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
 #[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable, TS))]
 #[cfg_attr(feature = "full", diesel(table_name = comment_aggregates))]
 #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))]
@@ -28,6 +28,7 @@ pub struct CommentAggregates {
   /// The total number of children in this comment branch.
   pub child_count: i32,
   pub hot_rank: i32,
+  pub controversy_rank: f64,
 }
 
 #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
@@ -72,7 +73,7 @@ pub struct PersonAggregates {
   pub comment_score: i64,
 }
 
-#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
+#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
 #[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable, TS))]
 #[cfg_attr(feature = "full", diesel(table_name = post_aggregates))]
 #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))]
@@ -98,6 +99,7 @@ pub struct PostAggregates {
   pub hot_rank_active: i32,
   pub community_id: CommunityId,
   pub creator_id: PersonId,
+  pub controversy_rank: f64,
 }
 
 #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
index acb069ca7d3711a585b647f32f84c8de46bc37e1..e5b86fe1977e076b983ec777e9a24447efbf26ee 100644 (file)
@@ -66,6 +66,7 @@ pub enum SortType {
   TopThreeMonths,
   TopSixMonths,
   TopNineMonths,
+  Controversial,
 }
 
 #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)]
@@ -77,6 +78,20 @@ pub enum CommentSortType {
   Top,
   New,
   Old,
+  Controversial,
+}
+
+#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)]
+#[cfg_attr(feature = "full", derive(TS))]
+#[cfg_attr(feature = "full", ts(export))]
+/// The person sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html
+pub enum PersonSortType {
+  New,
+  Old,
+  MostComments,
+  CommentScore,
+  PostScore,
+  PostCount,
 }
 
 #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
index faebe9999f42394a24f64b3a3959b73723d675e1..d6e2cf69c5846e4fc4275d44326f33945d9c0a71 100644 (file)
@@ -93,6 +93,7 @@ diesel::table! {
         published -> Timestamp,
         child_count -> Int4,
         hot_rank -> Int4,
+        controversy_rank -> Float8,
     }
 }
 
@@ -676,6 +677,7 @@ diesel::table! {
         hot_rank_active -> Int4,
         community_id -> Int4,
         creator_id -> Int4,
+        controversy_rank -> Float8,
     }
 }
 
index 94c867d6b897d8f6d0c920a197c803985391d36f..cd2005ad072f11bc13517921980ea62ab82a723d 100644 (file)
@@ -3,6 +3,7 @@ use crate::{
   diesel_migrations::MigrationHarness,
   newtypes::DbUrl,
   CommentSortType,
+  PersonSortType,
   SortType,
 };
 use activitypub_federation::{fetch::object_id::ObjectId, traits::Object};
@@ -347,6 +348,7 @@ pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType {
     SortType::Active | SortType::Hot => CommentSortType::Hot,
     SortType::New | SortType::NewComments | SortType::MostComments => CommentSortType::New,
     SortType::Old => CommentSortType::Old,
+    SortType::Controversial => CommentSortType::Controversial,
     SortType::TopHour
     | SortType::TopSixHour
     | SortType::TopTwelveHour
@@ -361,6 +363,16 @@ pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType {
   }
 }
 
+pub fn post_to_person_sort_type(sort: SortType) -> PersonSortType {
+  match sort {
+    SortType::Active | SortType::Hot | SortType::Controversial => PersonSortType::CommentScore,
+    SortType::New | SortType::NewComments => PersonSortType::New,
+    SortType::MostComments => PersonSortType::MostComments,
+    SortType::Old => PersonSortType::Old,
+    _ => PersonSortType::CommentScore,
+  }
+}
+
 static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| {
   Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
     .expect("compile email regex")
@@ -373,6 +385,10 @@ pub mod functions {
     fn hot_rank(score: BigInt, time: Timestamp) -> Integer;
   }
 
+  sql_function! {
+    fn controversy_rank(upvotes: BigInt, downvotes: BigInt, score: BigInt) -> Double;
+  }
+
   sql_function!(fn lower(x: Text) -> Text);
 }
 
index a09971dbe00824e26382913b53396142f43b1c09..0b1821c1b6b6a9abcb09c908e34d49e9fbc44ff7 100644 (file)
@@ -478,6 +478,7 @@ mod tests {
         published: agg.published,
         child_count: 0,
         hot_rank: 1728,
+        controversy_rank: 0.0,
       },
       my_vote: None,
       resolver: None,
index 6e06e1baaa72a8e0bdd01d47165bf44bb1300d4b..2d233438ab44e41bd9958fd7d609e941619a7237 100644 (file)
@@ -366,6 +366,9 @@ impl<'a> CommentQuery<'a> {
       CommentSortType::Hot => query
         .then_order_by(comment_aggregates::hot_rank.desc())
         .then_order_by(comment_aggregates::score.desc()),
+      CommentSortType::Controversial => {
+        query.then_order_by(comment_aggregates::controversy_rank.desc())
+      }
       CommentSortType::New => query.then_order_by(comment::published.desc()),
       CommentSortType::Old => query.then_order_by(comment::published.asc()),
       CommentSortType::Top => query.order_by(comment_aggregates::score.desc()),
@@ -948,6 +951,7 @@ mod tests {
         published: agg.published,
         child_count: 5,
         hot_rank: 1728,
+        controversy_rank: 0.0,
       },
     }
   }
index a53762e219643f26ab7a0e6c3f6f355b75581253..8c47d8c52321a308b4c9834b8413dfb56d958a25 100644 (file)
@@ -470,6 +470,7 @@ mod tests {
         featured_local: false,
         hot_rank: 1728,
         hot_rank_active: 1728,
+        controversy_rank: 0.0,
         community_id: inserted_post.community_id,
         creator_id: inserted_post.creator_id,
       },
index d2f6ab7598b1e707c6244a2521edcc97777bceea..e0f481edc3e4ad6714c4ea085a3f8eef828081e0 100644 (file)
@@ -422,6 +422,7 @@ impl<'a> PostQuery<'a> {
       SortType::Hot => query
         .then_order_by(post_aggregates::hot_rank.desc())
         .then_order_by(post_aggregates::published.desc()),
+      SortType::Controversial => query.then_order_by(post_aggregates::controversy_rank.desc()),
       SortType::New => query.then_order_by(post_aggregates::published.desc()),
       SortType::Old => query.then_order_by(post_aggregates::published.asc()),
       SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
@@ -1141,6 +1142,7 @@ mod tests {
         featured_local: false,
         hot_rank: 1728,
         hot_rank_active: 1728,
+        controversy_rank: 0.0,
         community_id: inserted_post.community_id,
         creator_id: inserted_post.creator_id,
       },
index 4032929097e17ba498946f50a57d81fff83757a8..536dd35973361e3acae57ccac702d054dc766e04 100644 (file)
@@ -25,7 +25,7 @@ use serde_with::skip_serializing_none;
 use ts_rs::TS;
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
 #[cfg_attr(feature = "full", derive(TS))]
 #[cfg_attr(feature = "full", ts(export))]
 /// A comment report view.
@@ -43,7 +43,7 @@ pub struct CommentReportView {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
 #[cfg_attr(feature = "full", derive(TS))]
 #[cfg_attr(feature = "full", ts(export))]
 /// A comment view.
@@ -71,7 +71,7 @@ pub struct LocalUserView {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
 #[cfg_attr(feature = "full", derive(TS))]
 #[cfg_attr(feature = "full", ts(export))]
 /// A post report view.
@@ -88,7 +88,7 @@ pub struct PostReportView {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
 #[cfg_attr(feature = "full", derive(TS))]
 #[cfg_attr(feature = "full", ts(export))]
 /// A post view.
index 4d7a8eac40c25ab247831b31a37d759af82c442f..406bfcb978cdb63c02af9131ad13656dd1fdd974 100644 (file)
@@ -266,6 +266,9 @@ impl CommentReplyQuery {
 
     query = match self.sort.unwrap_or(CommentSortType::New) {
       CommentSortType::Hot => query.then_order_by(comment_aggregates::hot_rank.desc()),
+      CommentSortType::Controversial => {
+        query.then_order_by(comment_aggregates::controversy_rank.desc())
+      }
       CommentSortType::New => query.then_order_by(comment_reply::published.desc()),
       CommentSortType::Old => query.then_order_by(comment_reply::published.asc()),
       CommentSortType::Top => query.order_by(comment_aggregates::score.desc()),
index 64dc090903004cbeddd1dc715f9479578261fddb..c31a2bd5d41c1ff2bee878676c5437a473bea7c9 100644 (file)
@@ -170,7 +170,8 @@ impl<'a> CommunityQuery<'a> {
       }
       New => query = query.order_by(community::published.desc()),
       Old => query = query.order_by(community::published.asc()),
-      MostComments => query = query.order_by(community_aggregates::comments.desc()),
+      // Controversial is temporary until a CommentSortType is created
+      MostComments | Controversial => query = query.order_by(community_aggregates::comments.desc()),
       TopAll | TopYear | TopNineMonths => {
         query = query.order_by(community_aggregates::subscribers.desc())
       }
index 3e142254ab937e8bbc1c36130ca28e4e2357056e..6bf107a3d8b2f266cf19c9f8770db5030214e218 100644 (file)
@@ -271,6 +271,9 @@ impl PersonMentionQuery {
 
     query = match self.sort.unwrap_or(CommentSortType::Hot) {
       CommentSortType::Hot => query.then_order_by(comment_aggregates::hot_rank.desc()),
+      CommentSortType::Controversial => {
+        query.then_order_by(comment_aggregates::controversy_rank.desc())
+      }
       CommentSortType::New => query.then_order_by(comment::published.desc()),
       CommentSortType::Old => query.then_order_by(comment::published.asc()),
       CommentSortType::Top => query.order_by(comment_aggregates::score.desc()),
index e6baa1fc6ba2b12682fa70e34e0a74aae681da58..908fbaab4328fb9489a78aaeada7fd83cd4ceba6 100644 (file)
@@ -1,6 +1,6 @@
 use crate::structs::PersonView;
 use diesel::{
-  dsl::{now, IntervalDsl},
+  dsl::now,
   result::Error,
   BoolExpressionMethods,
   ExpressionMethods,
@@ -16,7 +16,7 @@ use lemmy_db_schema::{
   source::person::Person,
   traits::JoinView,
   utils::{fuzzy_search, get_conn, limit_and_offset, DbPool},
-  SortType,
+  PersonSortType,
 };
 use std::iter::Iterator;
 
@@ -80,7 +80,7 @@ impl PersonView {
 
 #[derive(Default)]
 pub struct PersonQuery {
-  pub sort: Option<SortType>,
+  pub sort: Option<PersonSortType>,
   pub search_term: Option<String>,
   pub page: Option<i64>,
   pub limit: Option<i64>,
@@ -101,43 +101,13 @@ impl PersonQuery {
         .or_filter(person::display_name.ilike(searcher));
     }
 
-    query = match self.sort.unwrap_or(SortType::Hot) {
-      SortType::New | SortType::NewComments => query.order_by(person::published.desc()),
-      SortType::Old => query.order_by(person::published.asc()),
-      SortType::Hot | SortType::Active | SortType::TopAll => {
-        query.order_by(person_aggregates::comment_score.desc())
-      }
-      SortType::MostComments => query.order_by(person_aggregates::comment_count.desc()),
-      SortType::TopYear => query
-        .filter(person::published.gt(now - 1.years()))
-        .order_by(person_aggregates::comment_score.desc()),
-      SortType::TopMonth => query
-        .filter(person::published.gt(now - 1.months()))
-        .order_by(person_aggregates::comment_score.desc()),
-      SortType::TopWeek => query
-        .filter(person::published.gt(now - 1.weeks()))
-        .order_by(person_aggregates::comment_score.desc()),
-      SortType::TopDay => query
-        .filter(person::published.gt(now - 1.days()))
-        .order_by(person_aggregates::comment_score.desc()),
-      SortType::TopHour => query
-        .filter(person::published.gt(now - 1.hours()))
-        .order_by(person_aggregates::comment_score.desc()),
-      SortType::TopSixHour => query
-        .filter(person::published.gt(now - 6.hours()))
-        .order_by(person_aggregates::comment_score.desc()),
-      SortType::TopTwelveHour => query
-        .filter(person::published.gt(now - 12.hours()))
-        .order_by(person_aggregates::comment_score.desc()),
-      SortType::TopThreeMonths => query
-        .filter(person::published.gt(now - 3.months()))
-        .order_by(person_aggregates::comment_score.desc()),
-      SortType::TopSixMonths => query
-        .filter(person::published.gt(now - 6.months()))
-        .order_by(person_aggregates::comment_score.desc()),
-      SortType::TopNineMonths => query
-        .filter(person::published.gt(now - 9.months()))
-        .order_by(person_aggregates::comment_score.desc()),
+    query = match self.sort.unwrap_or(PersonSortType::CommentScore) {
+      PersonSortType::New => query.order_by(person::published.desc()),
+      PersonSortType::Old => query.order_by(person::published.asc()),
+      PersonSortType::MostComments => query.order_by(person_aggregates::comment_count.desc()),
+      PersonSortType::CommentScore => query.order_by(person_aggregates::comment_score.desc()),
+      PersonSortType::PostScore => query.order_by(person_aggregates::post_score.desc()),
+      PersonSortType::PostCount => query.order_by(person_aggregates::post_count.desc()),
     };
 
     let (limit, offset) = limit_and_offset(self.page, self.limit)?;
index 74d3fbe89ee0b4dbddb016a87e3805ba632b605e..35391776ba4667dc9f5a89268fb8d79494d7d8d0 100644 (file)
@@ -70,7 +70,7 @@ pub struct PersonBlockView {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
 #[cfg_attr(feature = "full", derive(TS))]
 #[cfg_attr(feature = "full", ts(export))]
 /// A person mention view.
@@ -90,7 +90,7 @@ pub struct PersonMentionView {
 }
 
 #[skip_serializing_none]
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
 #[cfg_attr(feature = "full", derive(TS))]
 #[cfg_attr(feature = "full", ts(export))]
 /// A comment reply view.
diff --git a/migrations/2023-07-26-000217_create_controversial_indexes/down.sql b/migrations/2023-07-26-000217_create_controversial_indexes/down.sql
new file mode 100644 (file)
index 0000000..a355546
--- /dev/null
@@ -0,0 +1,63 @@
+-- Update comment_aggregates_score trigger function to exclude controversy_rank update
+create or replace function comment_aggregates_score()
+returns trigger language plpgsql
+as $$
+begin
+  IF (TG_OP = 'INSERT') THEN
+    update comment_aggregates ca
+    set score = score + NEW.score,
+    upvotes = case when NEW.score = 1 then upvotes + 1 else upvotes end,
+    downvotes = case when NEW.score = -1 then downvotes + 1 else downvotes end
+    where ca.comment_id = NEW.comment_id;
+
+  ELSIF (TG_OP = 'DELETE') THEN
+    -- Join to comment because that comment may not exist anymore
+    update comment_aggregates ca
+    set score = score - OLD.score,
+    upvotes = case when OLD.score = 1 then upvotes - 1 else upvotes end,
+    downvotes = case when OLD.score = -1 then downvotes - 1 else downvotes end
+    from comment c
+    where ca.comment_id = c.id
+    and ca.comment_id = OLD.comment_id;
+
+  END IF;
+  return null;
+end $$;
+
+-- Update post_aggregates_score trigger function to exclude controversy_rank update
+create or replace function post_aggregates_score()
+returns trigger language plpgsql
+as $$
+begin
+  IF (TG_OP = 'INSERT') THEN
+    update post_aggregates pa
+    set score = score + NEW.score,
+    upvotes = case when NEW.score = 1 then upvotes + 1 else upvotes end,
+    downvotes = case when NEW.score = -1 then downvotes + 1 else downvotes end
+    where pa.post_id = NEW.post_id;
+
+  ELSIF (TG_OP = 'DELETE') THEN
+    -- Join to post because that post may not exist anymore
+    update post_aggregates pa
+    set score = score - OLD.score,
+    upvotes = case when OLD.score = 1 then upvotes - 1 else upvotes end,
+    downvotes = case when OLD.score = -1 then downvotes - 1 else downvotes end
+    from post p
+    where pa.post_id = p.id
+    and pa.post_id = OLD.post_id;
+  END IF;
+  return null;
+end $$;
+
+-- Drop the indexes
+drop index if exists idx_post_aggregates_featured_local_controversy;
+drop index if exists idx_post_aggregates_featured_community_controversy;
+drop index if exists idx_comment_aggregates_controversy;
+
+-- Remove the added columns from the tables
+alter table post_aggregates drop column controversy_rank;
+alter table comment_aggregates drop column controversy_rank;
+
+-- Remove function
+drop function controversy_rank(numeric, numeric);
+
diff --git a/migrations/2023-07-26-000217_create_controversial_indexes/up.sql b/migrations/2023-07-26-000217_create_controversial_indexes/up.sql
new file mode 100644 (file)
index 0000000..f50a706
--- /dev/null
@@ -0,0 +1,97 @@
+-- Need to add immutable to the controversy_rank function in order to index by it
+
+-- Controversy Rank:
+--      if downvotes <= 0 or upvotes <= 0:
+--          0 
+--      else:
+--          (upvotes + downvotes) * min(upvotes, downvotes) / max(upvotes, downvotes) 
+create or replace function controversy_rank(upvotes numeric, downvotes numeric)
+returns float as $$
+begin
+    if downvotes <= 0 or upvotes <= 0 then
+        return 0;
+    else
+        return (upvotes + downvotes) *
+            case when upvotes > downvotes
+                then downvotes::float / upvotes::float
+                else upvotes::float / downvotes::float
+            end;
+    end if;
+end; $$
+LANGUAGE plpgsql
+IMMUTABLE;
+
+-- Aggregates
+alter table post_aggregates add column controversy_rank float not null default 0;
+alter table comment_aggregates add column controversy_rank float not null default 0;
+
+-- Populate them initially
+-- Note: After initial population, these are updated with vote triggers
+update post_aggregates set controversy_rank = controversy_rank(upvotes::numeric, downvotes::numeric);
+update comment_aggregates set controversy_rank = controversy_rank(upvotes::numeric, downvotes::numeric);
+
+-- Create single column indexes
+create index idx_post_aggregates_featured_local_controversy on post_aggregates (featured_local desc, controversy_rank desc);
+create index idx_post_aggregates_featured_community_controversy on post_aggregates (featured_community desc, controversy_rank desc);
+create index idx_comment_aggregates_controversy on comment_aggregates (controversy_rank desc);
+
+-- Update post_aggregates_score trigger function to include controversy_rank update
+create or replace function post_aggregates_score()
+returns trigger language plpgsql
+as $$
+begin
+  IF (TG_OP = 'INSERT') THEN
+    update post_aggregates pa
+    set score = score + NEW.score,
+    upvotes = case when NEW.score = 1 then upvotes + 1 else upvotes end,
+    downvotes = case when NEW.score = -1 then downvotes + 1 else downvotes end,
+    controversy_rank = controversy_rank(pa.upvotes + case when NEW.score = 1 then 1 else 0 end::numeric, 
+                                         pa.downvotes + case when NEW.score = -1 then 1 else 0 end::numeric)
+    where pa.post_id = NEW.post_id;
+
+  ELSIF (TG_OP = 'DELETE') THEN
+    -- Join to post because that post may not exist anymore
+    update post_aggregates pa
+    set score = score - OLD.score,
+    upvotes = case when OLD.score = 1 then upvotes - 1 else upvotes end,
+    downvotes = case when OLD.score = -1 then downvotes - 1 else downvotes end,
+    controversy_rank = controversy_rank(pa.upvotes + case when NEW.score = 1 then 1 else 0 end::numeric, 
+                                         pa.downvotes + case when NEW.score = -1 then 1 else 0 end::numeric)
+    from post p
+    where pa.post_id = p.id
+    and pa.post_id = OLD.post_id;
+
+  END IF;
+  return null;
+end $$;
+
+-- Update comment_aggregates_score trigger function to include controversy_rank update
+create or replace function comment_aggregates_score()
+returns trigger language plpgsql
+as $$
+begin
+  IF (TG_OP = 'INSERT') THEN
+    update comment_aggregates ca
+    set score = score + NEW.score,
+    upvotes = case when NEW.score = 1 then upvotes + 1 else upvotes end,
+    downvotes = case when NEW.score = -1 then downvotes + 1 else downvotes end,
+    controversy_rank = controversy_rank(ca.upvotes + case when NEW.score = 1 then 1 else 0 end::numeric, 
+                                         ca.downvotes + case when NEW.score = -1 then 1 else 0 end::numeric)
+    where ca.comment_id = NEW.comment_id;
+
+  ELSIF (TG_OP = 'DELETE') THEN
+    -- Join to comment because that comment may not exist anymore
+    update comment_aggregates ca
+    set score = score - OLD.score,
+    upvotes = case when OLD.score = 1 then upvotes - 1 else upvotes end,
+    downvotes = case when OLD.score = -1 then downvotes - 1 else downvotes end,
+    controversy_rank = controversy_rank(ca.upvotes + case when NEW.score = 1 then 1 else 0 end::numeric, 
+                                         ca.downvotes + case when NEW.score = -1 then 1 else 0 end::numeric)
+    from comment c
+    where ca.comment_id = c.id
+    and ca.comment_id = OLD.comment_id;
+
+  END IF;
+  return null;
+end $$;
+