diff --git a/lemmy_db/src/aggregates/comment_aggregates.rs b/lemmy_db/src/aggregates/comment_aggregates.rs new file mode 100644 index 000000000..7ce52ed42 --- /dev/null +++ b/lemmy_db/src/aggregates/comment_aggregates.rs @@ -0,0 +1,23 @@ +use crate::schema::comment_aggregates; +use diesel::{result::Error, *}; +use serde::Serialize; + +#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)] +#[table_name = "comment_aggregates"] +pub struct CommentAggregates { + pub id: i32, + pub comment_id: i32, + pub score: i64, + pub upvotes: i64, + pub downvotes: i64, +} + +impl CommentAggregates { + pub fn read(conn: &PgConnection, comment_id: i32) -> Result { + comment_aggregates::table + .filter(comment_aggregates::comment_id.eq(comment_id)) + .first::(conn) + } +} + +// TODO add tests here diff --git a/lemmy_db/src/aggregates/mod.rs b/lemmy_db/src/aggregates/mod.rs index e033aede2..bdef6591d 100644 --- a/lemmy_db/src/aggregates/mod.rs +++ b/lemmy_db/src/aggregates/mod.rs @@ -1,3 +1,4 @@ +pub mod comment_aggregates; pub mod community_aggregates; pub mod post_aggregates; pub mod site_aggregates; diff --git a/lemmy_db/src/schema.rs b/lemmy_db/src/schema.rs index b0c57f5e1..5fa5e371c 100644 --- a/lemmy_db/src/schema.rs +++ b/lemmy_db/src/schema.rs @@ -34,6 +34,16 @@ table! { } } +table! { + comment_aggregates (id) { + id -> Int4, + comment_id -> Int4, + score -> Int8, + upvotes -> Int8, + downvotes -> Int8, + } +} + table! { comment_aggregates_fast (id) { id -> Int4, @@ -556,8 +566,61 @@ table! { } } +// These are necessary since diesel doesn't have self joins / aliases +table! { + comment_alias_1 (id) { + id -> Int4, + creator_id -> Int4, + post_id -> Int4, + parent_id -> Nullable, + content -> Text, + removed -> Bool, + read -> Bool, + published -> Timestamp, + updated -> Nullable, + deleted -> Bool, + ap_id -> Varchar, + local -> Bool, + } +} + +table! { + user_alias_1 (id) { + id -> Int4, + name -> Varchar, + preferred_username -> Nullable, + password_encrypted -> Text, + email -> Nullable, + avatar -> Nullable, + admin -> Bool, + banned -> Bool, + published -> Timestamp, + updated -> Nullable, + show_nsfw -> Bool, + theme -> Varchar, + default_sort_type -> Int2, + default_listing_type -> Int2, + lang -> Varchar, + show_avatars -> Bool, + send_notifications_to_email -> Bool, + matrix_user_id -> Nullable, + actor_id -> Varchar, + bio -> Nullable, + local -> Bool, + private_key -> Nullable, + public_key -> Nullable, + last_refreshed_at -> Timestamp, + banner -> Nullable, + deleted -> Bool, + } +} + +joinable!(comment_alias_1 -> user_alias_1 (creator_id)); +joinable!(comment -> comment_alias_1 (parent_id)); + joinable!(comment -> post (post_id)); joinable!(comment -> user_ (creator_id)); +joinable!(comment_aggregates -> comment (comment_id)); joinable!(comment_like -> comment (comment_id)); joinable!(comment_like -> post (post_id)); joinable!(comment_like -> user_ (user_id)); @@ -606,6 +669,7 @@ allow_tables_to_appear_in_same_query!( activity, category, comment, + comment_aggregates, comment_aggregates_fast, comment_like, comment_report, @@ -641,4 +705,6 @@ allow_tables_to_appear_in_same_query!( user_ban, user_fast, user_mention, + comment_alias_1, + user_alias_1, ); diff --git a/lemmy_db/src/source/comment.rs b/lemmy_db/src/source/comment.rs index dd4fb39de..239762599 100644 --- a/lemmy_db/src/source/comment.rs +++ b/lemmy_db/src/source/comment.rs @@ -1,13 +1,14 @@ use super::post::Post; use crate::{ naive_now, - schema::{comment, comment_like, comment_saved}, + schema::{comment, comment_alias_1, comment_like, comment_saved}, ApubObject, Crud, Likeable, Saveable, }; use diesel::{dsl::*, result::Error, *}; +use serde::Serialize; use url::{ParseError, Url}; // WITH RECURSIVE MyTree AS ( @@ -17,7 +18,7 @@ use url::{ParseError, Url}; // ) // SELECT * FROM MyTree; -#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug)] +#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize)] #[belongs_to(Post)] #[table_name = "comment"] pub struct Comment { @@ -35,6 +36,24 @@ pub struct Comment { pub local: bool, } +#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize)] +#[belongs_to(Post)] +#[table_name = "comment_alias_1"] +pub struct CommentAlias1 { + pub id: i32, + pub creator_id: i32, + pub post_id: i32, + pub parent_id: Option, + pub content: String, + pub removed: bool, + pub read: bool, // Whether the recipient has read the comment or not + pub published: chrono::NaiveDateTime, + pub updated: Option, + pub deleted: bool, + pub ap_id: String, + pub local: bool, +} + #[derive(Insertable, AsChangeset, Clone)] #[table_name = "comment"] pub struct CommentForm { diff --git a/lemmy_db/src/source/user.rs b/lemmy_db/src/source/user.rs index 5fdb56bbc..0bd68a509 100644 --- a/lemmy_db/src/source/user.rs +++ b/lemmy_db/src/source/user.rs @@ -1,7 +1,7 @@ use crate::{ is_email_regex, naive_now, - schema::{user_, user_::dsl::*}, + schema::{user_, user_::dsl::*, user_alias_1}, ApubObject, Crud, }; @@ -103,6 +103,98 @@ mod safe_type { } } +#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)] +#[table_name = "user_alias_1"] +pub struct UserAlias1 { + pub id: i32, + pub name: String, + pub preferred_username: Option, + pub password_encrypted: String, + pub email: Option, + pub avatar: Option, + pub admin: bool, + pub banned: bool, + pub published: chrono::NaiveDateTime, + pub updated: Option, + pub show_nsfw: bool, + pub theme: String, + pub default_sort_type: i16, + pub default_listing_type: i16, + pub lang: String, + pub show_avatars: bool, + pub send_notifications_to_email: bool, + pub matrix_user_id: Option, + pub actor_id: String, + pub bio: Option, + pub local: bool, + pub private_key: Option, + pub public_key: Option, + pub last_refreshed_at: chrono::NaiveDateTime, + pub banner: Option, + pub deleted: bool, +} + +#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)] +#[table_name = "user_alias_1"] +pub struct UserSafeAlias1 { + pub id: i32, + pub name: String, + pub preferred_username: Option, + pub avatar: Option, + pub admin: bool, + pub banned: bool, + pub published: chrono::NaiveDateTime, + pub updated: Option, + pub matrix_user_id: Option, + pub actor_id: String, + pub bio: Option, + pub local: bool, + pub banner: Option, + pub deleted: bool, +} + +mod safe_type_alias { + use crate::{schema::user_alias_1::columns::*, source::user::UserAlias1, ToSafe}; + type Columns = ( + id, + name, + preferred_username, + avatar, + admin, + banned, + published, + updated, + matrix_user_id, + actor_id, + bio, + local, + banner, + deleted, + ); + + impl ToSafe for UserAlias1 { + type SafeColumns = Columns; + fn safe_columns_tuple() -> Self::SafeColumns { + ( + id, + name, + preferred_username, + avatar, + admin, + banned, + published, + updated, + matrix_user_id, + actor_id, + bio, + local, + banner, + deleted, + ) + } + } +} + #[derive(Insertable, AsChangeset, Clone)] #[table_name = "user_"] pub struct UserForm { diff --git a/lemmy_db/src/views/comment_view.rs b/lemmy_db/src/views/comment_view.rs new file mode 100644 index 000000000..3e812699a --- /dev/null +++ b/lemmy_db/src/views/comment_view.rs @@ -0,0 +1,662 @@ +use crate::{ + aggregates::comment_aggregates::CommentAggregates, + functions::hot_rank, + fuzzy_search, + limit_and_offset, + schema::{ + comment, + comment_aggregates, + comment_alias_1, + comment_like, + comment_saved, + community, + community_follower, + community_user_ban, + post, + user_, + user_alias_1, + }, + source::{ + comment::{Comment, CommentAlias1, CommentSaved}, + community::{Community, CommunityFollower, CommunitySafe, CommunityUserBan}, + post::Post, + user::{UserAlias1, UserSafe, UserSafeAlias1, User_}, + }, + views::ViewToVec, + ListingType, + MaybeOptional, + SortType, + ToSafe, +}; +use diesel::{result::Error, *}; +use serde::Serialize; + +#[derive(Debug, PartialEq, Serialize, Clone)] +pub struct CommentView { + pub comment: Comment, + pub creator: UserSafe, + pub recipient: Option, // Left joins to comment and user + pub post: Post, + pub community: CommunitySafe, + pub counts: CommentAggregates, + pub creator_banned_from_community: bool, // Left Join to CommunityUserBan + pub subscribed: bool, // Left join to CommunityFollower + pub saved: bool, // Left join to CommentSaved + pub my_vote: Option, // Left join to CommentLike +} + +type CommentViewTuple = ( + Comment, + UserSafe, + Option, + Option, + Post, + CommunitySafe, + CommentAggregates, + Option, + Option, + Option, + Option, +); + +impl CommentView { + pub fn read( + conn: &PgConnection, + comment_id: i32, + my_user_id: Option, + ) -> Result { + // The left join below will return None in this case + let user_id_join = my_user_id.unwrap_or(-1); + + let ( + comment, + creator, + _parent_comment, + recipient, + post, + community, + counts, + creator_banned_from_community, + subscribed, + saved, + my_vote, + ) = comment::table + .find(comment_id) + .inner_join(user_::table) + // recipient here + .left_join(comment_alias_1::table.on(comment_alias_1::id.nullable().eq(comment::parent_id))) + .left_join(user_alias_1::table.on(user_alias_1::id.eq(comment_alias_1::creator_id))) + .inner_join(post::table) + .inner_join(community::table.on(post::community_id.eq(community::id))) + .inner_join(comment_aggregates::table) + .left_join( + community_user_ban::table.on( + community::id + .eq(community_user_ban::community_id) + .and(community_user_ban::user_id.eq(comment::creator_id)), + ), + ) + .left_join( + community_follower::table.on( + post::community_id + .eq(community_follower::community_id) + .and(community_follower::user_id.eq(user_id_join)), + ), + ) + .left_join( + comment_saved::table.on( + comment::id + .eq(comment_saved::comment_id) + .and(comment_saved::user_id.eq(user_id_join)), + ), + ) + .left_join( + comment_like::table.on( + comment::id + .eq(comment_like::comment_id) + .and(comment_like::user_id.eq(user_id_join)), + ), + ) + .select(( + comment::all_columns, + User_::safe_columns_tuple(), + comment_alias_1::all_columns.nullable(), + UserAlias1::safe_columns_tuple().nullable(), + post::all_columns, + Community::safe_columns_tuple(), + comment_aggregates::all_columns, + community_user_ban::all_columns.nullable(), + community_follower::all_columns.nullable(), + comment_saved::all_columns.nullable(), + comment_like::score.nullable(), + )) + .first::(conn)?; + + Ok(CommentView { + comment, + recipient, + post, + creator, + community, + counts, + creator_banned_from_community: creator_banned_from_community.is_some(), + subscribed: subscribed.is_some(), + saved: saved.is_some(), + my_vote, + }) + } +} + +mod join_types { + use crate::schema::{ + comment, + comment_aggregates, + comment_alias_1, + comment_like, + comment_saved, + community, + community_follower, + community_user_ban, + post, + user_, + user_alias_1, + }; + use diesel::{ + pg::Pg, + query_builder::BoxedSelectStatement, + query_source::joins::{Inner, Join, JoinOn, LeftOuter}, + sql_types::*, + }; + + // /// TODO awful, but necessary because of the boxed join + pub(super) type BoxedCommentJoin<'a> = BoxedSelectStatement< + 'a, + ( + ( + Integer, + Integer, + Integer, + Nullable, + Text, + Bool, + Bool, + Timestamp, + Nullable, + Bool, + Text, + Bool, + ), + ( + Integer, + Text, + Nullable, + Nullable, + Bool, + Bool, + Timestamp, + Nullable, + Nullable, + Text, + Nullable, + Bool, + Nullable, + Bool, + ), + Nullable<( + Integer, + Integer, + Integer, + Nullable, + Text, + Bool, + Bool, + Timestamp, + Nullable, + Bool, + Text, + Bool, + )>, + Nullable<( + Integer, + Text, + Nullable, + Nullable, + Bool, + Bool, + Timestamp, + Nullable, + Nullable, + Text, + Nullable, + Bool, + Nullable, + Bool, + )>, + ( + Integer, + Text, + Nullable, + Nullable, + Integer, + Integer, + Bool, + Bool, + Timestamp, + Nullable, + Bool, + Bool, + Bool, + Nullable, + Nullable, + Nullable, + Nullable, + Text, + Bool, + ), + ( + Integer, + Text, + Text, + Nullable, + Integer, + Integer, + Bool, + Timestamp, + Nullable, + Bool, + Bool, + Text, + Bool, + Nullable, + Nullable, + ), + (Integer, Integer, BigInt, BigInt, BigInt), + Nullable<(Integer, Integer, Integer, Timestamp)>, + Nullable<(Integer, Integer, Integer, Timestamp, Nullable)>, + Nullable<(Integer, Integer, Integer, Timestamp)>, + Nullable, + ), + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join, + diesel::expression::operators::Eq< + diesel::expression::nullable::Nullable< + comment::columns::creator_id, + >, + diesel::expression::nullable::Nullable< + user_::columns::id, + >, + >, + >, + comment_alias_1::table, + LeftOuter, + >, + diesel::expression::operators::Eq< + diesel::expression::nullable::Nullable< + comment_alias_1::columns::id, + >, + comment::columns::parent_id, + >, + >, + user_alias_1::table, + LeftOuter, + >, + diesel::expression::operators::Eq< + user_alias_1::columns::id, + comment_alias_1::columns::creator_id, + >, + >, + post::table, + Inner, + >, + diesel::expression::operators::Eq< + diesel::expression::nullable::Nullable, + diesel::expression::nullable::Nullable, + >, + >, + community::table, + Inner, + >, + diesel::expression::operators::Eq< + post::columns::community_id, + community::columns::id, + >, + >, + comment_aggregates::table, + Inner, + >, + diesel::expression::operators::Eq< + diesel::expression::nullable::Nullable< + comment_aggregates::columns::comment_id, + >, + diesel::expression::nullable::Nullable, + >, + >, + community_user_ban::table, + LeftOuter, + >, + diesel::expression::operators::And< + diesel::expression::operators::Eq< + community::columns::id, + community_user_ban::columns::community_id, + >, + diesel::expression::operators::Eq< + community_user_ban::columns::user_id, + comment::columns::creator_id, + >, + >, + >, + community_follower::table, + LeftOuter, + >, + diesel::expression::operators::And< + diesel::expression::operators::Eq< + post::columns::community_id, + community_follower::columns::community_id, + >, + diesel::expression::operators::Eq< + community_follower::columns::user_id, + diesel::expression::bound::Bound, + >, + >, + >, + comment_saved::table, + LeftOuter, + >, + diesel::expression::operators::And< + diesel::expression::operators::Eq< + comment::columns::id, + comment_saved::columns::comment_id, + >, + diesel::expression::operators::Eq< + comment_saved::columns::user_id, + diesel::expression::bound::Bound, + >, + >, + >, + comment_like::table, + LeftOuter, + >, + diesel::expression::operators::And< + diesel::expression::operators::Eq, + diesel::expression::operators::Eq< + comment_like::columns::user_id, + diesel::expression::bound::Bound, + >, + >, + >, + Pg, + >; +} + +pub struct CommentQueryBuilder<'a> { + conn: &'a PgConnection, + query: join_types::BoxedCommentJoin<'a>, + listing_type: ListingType, + sort: &'a SortType, + for_community_id: Option, + for_community_name: Option, + for_post_id: Option, + for_creator_id: Option, + for_recipient_id: Option, + search_term: Option, + saved_only: bool, + unread_only: bool, + page: Option, + limit: Option, +} + +impl<'a> CommentQueryBuilder<'a> { + pub fn create(conn: &'a PgConnection, my_user_id: Option) -> Self { + // The left join below will return None in this case + let user_id_join = my_user_id.unwrap_or(-1); + + let query = comment::table + .inner_join(user_::table) + // recipient here + .left_join(comment_alias_1::table.on(comment_alias_1::id.nullable().eq(comment::parent_id))) + .left_join(user_alias_1::table.on(user_alias_1::id.eq(comment_alias_1::creator_id))) + .inner_join(post::table) + .inner_join(community::table.on(post::community_id.eq(community::id))) + .inner_join(comment_aggregates::table) + .left_join( + community_user_ban::table.on( + community::id + .eq(community_user_ban::community_id) + .and(community_user_ban::user_id.eq(comment::creator_id)), + ), + ) + .left_join( + community_follower::table.on( + post::community_id + .eq(community_follower::community_id) + .and(community_follower::user_id.eq(user_id_join)), + ), + ) + .left_join( + comment_saved::table.on( + comment::id + .eq(comment_saved::comment_id) + .and(comment_saved::user_id.eq(user_id_join)), + ), + ) + .left_join( + comment_like::table.on( + comment::id + .eq(comment_like::comment_id) + .and(comment_like::user_id.eq(user_id_join)), + ), + ) + .select(( + comment::all_columns, + User_::safe_columns_tuple(), + comment_alias_1::all_columns.nullable(), + UserAlias1::safe_columns_tuple().nullable(), + post::all_columns, + Community::safe_columns_tuple(), + comment_aggregates::all_columns, + community_user_ban::all_columns.nullable(), + community_follower::all_columns.nullable(), + comment_saved::all_columns.nullable(), + comment_like::score.nullable(), + )) + .into_boxed(); + + CommentQueryBuilder { + conn, + query, + listing_type: ListingType::All, + sort: &SortType::New, + for_community_id: None, + for_community_name: None, + for_post_id: None, + for_creator_id: None, + for_recipient_id: None, + search_term: None, + saved_only: false, + unread_only: false, + page: None, + limit: None, + } + } + + pub fn listing_type(mut self, listing_type: ListingType) -> Self { + self.listing_type = listing_type; + self + } + + pub fn sort(mut self, sort: &'a SortType) -> Self { + self.sort = sort; + self + } + + pub fn for_post_id>(mut self, for_post_id: T) -> Self { + self.for_post_id = for_post_id.get_optional(); + self + } + + pub fn for_creator_id>(mut self, for_creator_id: T) -> Self { + self.for_creator_id = for_creator_id.get_optional(); + self + } + + pub fn for_recipient_id>(mut self, for_recipient_id: T) -> Self { + self.for_creator_id = for_recipient_id.get_optional(); + self + } + + pub fn for_community_id>(mut self, for_community_id: T) -> Self { + self.for_community_id = for_community_id.get_optional(); + self + } + + pub fn for_community_name>(mut self, for_community_name: T) -> Self { + self.for_community_name = for_community_name.get_optional(); + self + } + + pub fn search_term>(mut self, search_term: T) -> Self { + self.search_term = search_term.get_optional(); + self + } + + pub fn saved_only(mut self, saved_only: bool) -> Self { + self.saved_only = saved_only; + self + } + + pub fn unread_only(mut self, unread_only: bool) -> Self { + self.unread_only = unread_only; + self + } + + pub fn page>(mut self, page: T) -> Self { + self.page = page.get_optional(); + self + } + + pub fn limit>(mut self, limit: T) -> Self { + self.limit = limit.get_optional(); + self + } + + pub fn list(self) -> Result, Error> { + use diesel::dsl::*; + + let mut query = self.query; + + // The replies + if let Some(for_recipient_id) = self.for_recipient_id { + query = query + // TODO needs lots of testing + .filter(user_alias_1::id.eq(for_recipient_id)) + .filter(comment::deleted.eq(false)) + .filter(comment::removed.eq(false)); + } + + if self.unread_only { + query = query.filter(comment::read.eq(false)); + } + + if let Some(for_creator_id) = self.for_creator_id { + query = query.filter(comment::creator_id.eq(for_creator_id)); + }; + + if let Some(for_community_id) = self.for_community_id { + query = query.filter(post::community_id.eq(for_community_id)); + } + + if let Some(for_community_name) = self.for_community_name { + query = query + .filter(community::name.eq(for_community_name)) + .filter(comment::local.eq(true)); + } + + if let Some(for_post_id) = self.for_post_id { + query = query.filter(comment::post_id.eq(for_post_id)); + }; + + if let Some(search_term) = self.search_term { + query = query.filter(comment::content.ilike(fuzzy_search(&search_term))); + }; + + query = match self.listing_type { + // ListingType::Subscribed => query.filter(community_follower::subscribed.eq(true)), + ListingType::Subscribed => query.filter(community_follower::user_id.is_not_null()), // TODO could be this: and(community_follower::user_id.eq(user_id_join)), + ListingType::Local => query.filter(community::local.eq(true)), + _ => query, + }; + + if self.saved_only { + query = query.filter(comment_saved::id.is_not_null()); + } + + query = match self.sort { + SortType::Hot | SortType::Active => query + .order_by(hot_rank(comment_aggregates::score, comment::published).desc()) + .then_order_by(comment::published.desc()), + SortType::New => query.order_by(comment::published.desc()), + SortType::TopAll => query.order_by(comment_aggregates::score.desc()), + SortType::TopYear => query + .filter(comment::published.gt(now - 1.years())) + .order_by(comment_aggregates::score.desc()), + SortType::TopMonth => query + .filter(comment::published.gt(now - 1.months())) + .order_by(comment_aggregates::score.desc()), + SortType::TopWeek => query + .filter(comment::published.gt(now - 1.weeks())) + .order_by(comment_aggregates::score.desc()), + SortType::TopDay => query + .filter(comment::published.gt(now - 1.days())) + .order_by(comment_aggregates::score.desc()), + }; + + let (limit, offset) = limit_and_offset(self.page, self.limit); + + // Note: deleted and removed comments are done on the front side + let res = query + .limit(limit) + .offset(offset) + .load::(self.conn)?; + + Ok(CommentView::to_vec(res)) + } +} + +impl ViewToVec for CommentView { + type DbTuple = CommentViewTuple; + fn to_vec(posts: Vec) -> Vec { + posts + .iter() + .map(|a| Self { + comment: a.0.to_owned(), + creator: a.1.to_owned(), + recipient: a.3.to_owned(), + post: a.4.to_owned(), + community: a.5.to_owned(), + counts: a.6.to_owned(), + creator_banned_from_community: a.7.is_some(), + subscribed: a.8.is_some(), + saved: a.9.is_some(), + my_vote: a.10, + }) + .collect::>() + } +} diff --git a/lemmy_db/src/views/mod.rs b/lemmy_db/src/views/mod.rs index 465e5cffa..a3295ec00 100644 --- a/lemmy_db/src/views/mod.rs +++ b/lemmy_db/src/views/mod.rs @@ -1,3 +1,4 @@ +pub mod comment_view; pub mod community_follower_view; pub mod community_moderator_view; pub mod community_user_ban_view; diff --git a/lemmy_db/src/views/post_view.rs b/lemmy_db/src/views/post_view.rs index 4888f9cfb..9791d0a8b 100644 --- a/lemmy_db/src/views/post_view.rs +++ b/lemmy_db/src/views/post_view.rs @@ -33,12 +33,12 @@ pub struct PostView { pub post: Post, pub creator: UserSafe, pub community: CommunitySafe, - pub counts: PostAggregates, - pub subscribed: bool, // Left join to CommunityFollower pub creator_banned_from_community: bool, // Left Join to CommunityUserBan - pub saved: bool, // Left join to PostSaved - pub read: bool, // Left join to PostRead - pub my_vote: Option, // Left join to PostLike + pub counts: PostAggregates, + pub subscribed: bool, // Left join to CommunityFollower + pub saved: bool, // Left join to PostSaved + pub read: bool, // Left join to PostRead + pub my_vote: Option, // Left join to PostLike } type PostViewTuple = ( @@ -76,7 +76,7 @@ impl PostView { community_user_ban::table.on( post::community_id .eq(community_user_ban::community_id) - .and(community_user_ban::user_id.eq(community::creator_id)), + .and(community_user_ban::user_id.eq(post::creator_id)), ), ) .inner_join(post_aggregates::table) diff --git a/migrations/2020-12-14-020038_create_comment_aggregates/down.sql b/migrations/2020-12-14-020038_create_comment_aggregates/down.sql new file mode 100644 index 000000000..6fd9ddc25 --- /dev/null +++ b/migrations/2020-12-14-020038_create_comment_aggregates/down.sql @@ -0,0 +1,7 @@ +-- comment aggregates +drop table comment_aggregates; +drop trigger comment_aggregates_comment on comment; +drop trigger comment_aggregates_score on comment_like; +drop function + comment_aggregates_comment, + comment_aggregates_score; diff --git a/migrations/2020-12-14-020038_create_comment_aggregates/up.sql b/migrations/2020-12-14-020038_create_comment_aggregates/up.sql new file mode 100644 index 000000000..1a168beca --- /dev/null +++ b/migrations/2020-12-14-020038_create_comment_aggregates/up.sql @@ -0,0 +1,82 @@ +-- Add comment aggregates +create table comment_aggregates ( + id serial primary key, + comment_id int references comment on update cascade on delete cascade not null, + score bigint not null default 0, + upvotes bigint not null default 0, + downvotes bigint not null default 0, + unique (comment_id) +); + +insert into comment_aggregates (comment_id, score, upvotes, downvotes) + select + c.id, + COALESCE(cl.total, 0::bigint) AS score, + COALESCE(cl.up, 0::bigint) AS upvotes, + COALESCE(cl.down, 0::bigint) AS downvotes + from comment c + left join ( select l.comment_id as id, + sum(l.score) as total, + count( + case + when l.score = 1 then 1 + else null::integer + end) as up, + count( + case + when l.score = '-1'::integer then 1 + else null::integer + end) as down + from comment_like l + group by l.comment_id) cl on cl.id = c.id; + +-- Add comment aggregate triggers + +-- initial comment add +create function comment_aggregates_comment() +returns trigger language plpgsql +as $$ +begin + IF (TG_OP = 'INSERT') THEN + insert into comment_aggregates (comment_id) values (NEW.id); + ELSIF (TG_OP = 'DELETE') THEN + delete from comment_aggregates where comment_id = OLD.id; + END IF; + return null; +end $$; + +create trigger comment_aggregates_comment +after insert or delete on comment +for each row +execute procedure comment_aggregates_comment(); + +-- comment score +create 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 $$; + +create trigger comment_aggregates_score +after insert or delete on comment_like +for each row +execute procedure comment_aggregates_score();