diff --git a/lemmy_db/src/aggregates/community_aggregates.rs b/lemmy_db/src/aggregates/community_aggregates.rs index 9a8ea3658..04257c506 100644 --- a/lemmy_db/src/aggregates/community_aggregates.rs +++ b/lemmy_db/src/aggregates/community_aggregates.rs @@ -9,13 +9,258 @@ pub struct CommunityAggregates { pub community_id: i32, pub subscribers: i64, pub posts: i64, - pub counts: i64, + pub comments: i64, } impl CommunityAggregates { - pub fn read(conn: &PgConnection, id: i32) -> Result { - community_aggregates::table.find(id).first::(conn) + pub fn read(conn: &PgConnection, community_id: i32) -> Result { + community_aggregates::table + .filter(community_aggregates::community_id.eq(community_id)) + .first::(conn) } } -// TODO add unit tests, to make sure triggers are working +#[cfg(test)] +mod tests { + use crate::{ + aggregates::community_aggregates::CommunityAggregates, + comment::{Comment, CommentForm}, + community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm}, + post::{Post, PostForm}, + tests::establish_unpooled_connection, + user::{UserForm, User_}, + Crud, + Followable, + ListingType, + SortType, + }; + + #[test] + fn test_crud() { + let conn = establish_unpooled_connection(); + + let new_user = UserForm { + name: "thommy_community_agg".into(), + preferred_username: None, + password_encrypted: "nope".into(), + email: None, + matrix_user_id: None, + avatar: None, + banner: None, + admin: false, + banned: Some(false), + published: None, + updated: None, + show_nsfw: false, + theme: "browser".into(), + default_sort_type: SortType::Hot as i16, + default_listing_type: ListingType::Subscribed as i16, + lang: "browser".into(), + show_avatars: true, + send_notifications_to_email: false, + actor_id: None, + bio: None, + local: true, + private_key: None, + public_key: None, + last_refreshed_at: None, + }; + + let inserted_user = User_::create(&conn, &new_user).unwrap(); + + let another_user = UserForm { + name: "jerry_community_agg".into(), + preferred_username: None, + password_encrypted: "nope".into(), + email: None, + matrix_user_id: None, + avatar: None, + banner: None, + admin: false, + banned: Some(false), + published: None, + updated: None, + show_nsfw: false, + theme: "browser".into(), + default_sort_type: SortType::Hot as i16, + default_listing_type: ListingType::Subscribed as i16, + lang: "browser".into(), + show_avatars: true, + send_notifications_to_email: false, + actor_id: None, + bio: None, + local: true, + private_key: None, + public_key: None, + last_refreshed_at: None, + }; + + let another_inserted_user = User_::create(&conn, &another_user).unwrap(); + + let new_community = CommunityForm { + name: "TIL_community_agg".into(), + creator_id: inserted_user.id, + title: "nada".to_owned(), + description: None, + category_id: 1, + nsfw: false, + removed: None, + deleted: None, + updated: None, + actor_id: None, + local: true, + private_key: None, + public_key: None, + last_refreshed_at: None, + published: None, + icon: None, + banner: None, + }; + + let inserted_community = Community::create(&conn, &new_community).unwrap(); + + let another_community = CommunityForm { + name: "TIL_community_agg_2".into(), + creator_id: inserted_user.id, + title: "nada".to_owned(), + description: None, + category_id: 1, + nsfw: false, + removed: None, + deleted: None, + updated: None, + actor_id: None, + local: true, + private_key: None, + public_key: None, + last_refreshed_at: None, + published: None, + icon: None, + banner: None, + }; + + let another_inserted_community = Community::create(&conn, &another_community).unwrap(); + + let first_user_follow = CommunityFollowerForm { + community_id: inserted_community.id, + user_id: inserted_user.id, + pending: false, + }; + + CommunityFollower::follow(&conn, &first_user_follow).unwrap(); + + let second_user_follow = CommunityFollowerForm { + community_id: inserted_community.id, + user_id: another_inserted_user.id, + pending: false, + }; + + CommunityFollower::follow(&conn, &second_user_follow).unwrap(); + + let another_community_follow = CommunityFollowerForm { + community_id: another_inserted_community.id, + user_id: inserted_user.id, + pending: false, + }; + + CommunityFollower::follow(&conn, &another_community_follow).unwrap(); + + let new_post = PostForm { + name: "A test post".into(), + url: None, + body: None, + creator_id: inserted_user.id, + community_id: inserted_community.id, + removed: None, + deleted: None, + locked: None, + stickied: None, + nsfw: false, + updated: None, + embed_title: None, + embed_description: None, + embed_html: None, + thumbnail_url: None, + ap_id: None, + local: true, + published: None, + }; + + let inserted_post = Post::create(&conn, &new_post).unwrap(); + + let comment_form = CommentForm { + content: "A test comment".into(), + creator_id: inserted_user.id, + post_id: inserted_post.id, + removed: None, + deleted: None, + read: None, + parent_id: None, + published: None, + updated: None, + ap_id: None, + local: true, + }; + + let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); + + let child_comment_form = CommentForm { + content: "A test comment".into(), + creator_id: inserted_user.id, + post_id: inserted_post.id, + removed: None, + deleted: None, + read: None, + parent_id: Some(inserted_comment.id), + published: None, + updated: None, + ap_id: None, + local: true, + }; + + let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); + + let community_aggregates_before_delete = + CommunityAggregates::read(&conn, inserted_community.id).unwrap(); + + assert_eq!(2, community_aggregates_before_delete.subscribers); + assert_eq!(1, community_aggregates_before_delete.posts); + assert_eq!(2, community_aggregates_before_delete.comments); + + // Test the other community + let another_community_aggs = + CommunityAggregates::read(&conn, another_inserted_community.id).unwrap(); + assert_eq!(1, another_community_aggs.subscribers); + assert_eq!(0, another_community_aggs.posts); + assert_eq!(0, another_community_aggs.comments); + + // Unfollow test + CommunityFollower::unfollow(&conn, &second_user_follow).unwrap(); + let after_unfollow = CommunityAggregates::read(&conn, inserted_community.id).unwrap(); + assert_eq!(1, after_unfollow.subscribers); + + // Follow again just for the later tests + CommunityFollower::follow(&conn, &second_user_follow).unwrap(); + let after_follow_again = CommunityAggregates::read(&conn, inserted_community.id).unwrap(); + assert_eq!(2, after_follow_again.subscribers); + + // Remove a parent comment (the comment count should also be 0) + Post::delete(&conn, inserted_post.id).unwrap(); + let after_parent_post_delete = CommunityAggregates::read(&conn, inserted_community.id).unwrap(); + assert_eq!(0, after_parent_post_delete.comments); + assert_eq!(0, after_parent_post_delete.posts); + + // Remove the 2nd user + User_::delete(&conn, another_inserted_user.id).unwrap(); + let after_user_delete = CommunityAggregates::read(&conn, inserted_community.id).unwrap(); + assert_eq!(1, after_user_delete.subscribers); + + // This should delete all the associated rows, and fire triggers + let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap(); + assert_eq!(1, user_num_deleted); + + // Should be none found, since the creator was deleted + let after_delete = CommunityAggregates::read(&conn, inserted_community.id); + assert!(after_delete.is_err()); + } +} diff --git a/lemmy_db/src/aggregates/site_aggregates.rs b/lemmy_db/src/aggregates/site_aggregates.rs index fdd4d1c46..76b45555b 100644 --- a/lemmy_db/src/aggregates/site_aggregates.rs +++ b/lemmy_db/src/aggregates/site_aggregates.rs @@ -108,7 +108,9 @@ mod tests { published: None, }; + // Insert two of those posts let inserted_post = Post::create(&conn, &new_post).unwrap(); + let _inserted_post_again = Post::create(&conn, &new_post).unwrap(); let comment_form = CommentForm { content: "A test comment".into(), @@ -124,6 +126,7 @@ mod tests { local: true, }; + // Insert two of those comments let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let child_comment_form = CommentForm { @@ -146,9 +149,15 @@ mod tests { assert_eq!(1, site_aggregates_before_delete.users); assert_eq!(1, site_aggregates_before_delete.communities); - assert_eq!(1, site_aggregates_before_delete.posts); + assert_eq!(2, site_aggregates_before_delete.posts); assert_eq!(2, site_aggregates_before_delete.comments); + // Try a post delete + Post::delete(&conn, inserted_post.id).unwrap(); + let site_aggregates_after_post_delete = SiteAggregates::read(&conn).unwrap(); + assert_eq!(1, site_aggregates_after_post_delete.posts); + assert_eq!(0, site_aggregates_after_post_delete.comments); + // This shouuld delete all the associated rows, and fire triggers let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap(); assert_eq!(1, user_num_deleted); diff --git a/lemmy_db/src/aggregates/user_aggregates.rs b/lemmy_db/src/aggregates/user_aggregates.rs index 622bce113..e962c0dd4 100644 --- a/lemmy_db/src/aggregates/user_aggregates.rs +++ b/lemmy_db/src/aggregates/user_aggregates.rs @@ -167,7 +167,7 @@ mod tests { let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); - let comment_like = CommentLikeForm { + let mut comment_like = CommentLikeForm { comment_id: inserted_comment.id, user_id: inserted_user.id, post_id: inserted_post.id, @@ -176,7 +176,7 @@ mod tests { let _inserted_comment_like = CommentLike::like(&conn, &comment_like).unwrap(); - let child_comment_form = CommentForm { + let mut child_comment_form = CommentForm { content: "A test comment".into(), creator_id: inserted_user.id, post_id: inserted_post.id, @@ -219,6 +219,23 @@ mod tests { assert_eq!(0, after_parent_comment_delete.comment_count); assert_eq!(0, after_parent_comment_delete.comment_score); + // Add in the two comments again, then delete the post. + let new_parent_comment = Comment::create(&conn, &comment_form).unwrap(); + child_comment_form.parent_id = Some(new_parent_comment.id); + Comment::create(&conn, &child_comment_form).unwrap(); + comment_like.comment_id = new_parent_comment.id; + CommentLike::like(&conn, &comment_like).unwrap(); + let after_comment_add = UserAggregates::read(&conn, inserted_user.id).unwrap(); + assert_eq!(2, after_comment_add.comment_count); + assert_eq!(1, after_comment_add.comment_score); + + Post::delete(&conn, inserted_post.id).unwrap(); + let after_post_delete = UserAggregates::read(&conn, inserted_user.id).unwrap(); + assert_eq!(0, after_post_delete.comment_score); + assert_eq!(0, after_post_delete.comment_count); + assert_eq!(0, after_post_delete.post_score); + assert_eq!(0, after_post_delete.post_count); + // This should delete all the associated rows, and fire triggers let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap(); assert_eq!(1, user_num_deleted); diff --git a/migrations/2020-12-02-152437_create_site_aggregates/up.sql b/migrations/2020-12-02-152437_create_site_aggregates/up.sql index 0a5d208a6..b95723476 100644 --- a/migrations/2020-12-02-152437_create_site_aggregates/up.sql +++ b/migrations/2020-12-02-152437_create_site_aggregates/up.sql @@ -31,6 +31,7 @@ end $$; create trigger site_aggregates_user after insert or delete on user_ +for each row execute procedure site_aggregates_user(); -- post diff --git a/migrations/2020-12-03-035643_create_user_aggregates/up.sql b/migrations/2020-12-03-035643_create_user_aggregates/up.sql index 1bebfe305..7b4c83af2 100644 --- a/migrations/2020-12-03-035643_create_user_aggregates/up.sql +++ b/migrations/2020-12-03-035643_create_user_aggregates/up.sql @@ -80,7 +80,7 @@ begin left join post_like pl on p.id = pl.post_id group by u.id ) pd - where ua.user_id = pd.id; + where ua.user_id = OLD.creator_id; END IF; return null; @@ -97,7 +97,6 @@ returns trigger language plpgsql as $$ begin IF (TG_OP = 'INSERT') THEN - -- TODO not sure if this is working right -- Need to get the post creator, not the voter update user_aggregates ua set post_score = post_score + NEW.score @@ -143,7 +142,7 @@ begin left join comment_like cl on c.id = cl.comment_id group by u.id ) cd - where ua.user_id = cd.id; + where ua.user_id = OLD.creator_id; END IF; return null; end $$; diff --git a/migrations/2020-12-04-183345_create_community_aggregates/down.sql b/migrations/2020-12-04-183345_create_community_aggregates/down.sql index ac2872d1f..fc0ffd21a 100644 --- a/migrations/2020-12-04-183345_create_community_aggregates/down.sql +++ b/migrations/2020-12-04-183345_create_community_aggregates/down.sql @@ -1,9 +1,11 @@ -- community aggregates drop table community_aggregates; +drop trigger community_aggregates_community on community; drop trigger community_aggregates_post_count on post; drop trigger community_aggregates_comment_count on comment; drop trigger community_aggregates_subscriber_count on community_follower; drop function + community_aggregates_community, community_aggregates_post_count, community_aggregates_comment_count, community_aggregates_subscriber_count; diff --git a/migrations/2020-12-04-183345_create_community_aggregates/up.sql b/migrations/2020-12-04-183345_create_community_aggregates/up.sql index 34274b0d9..18a62298f 100644 --- a/migrations/2020-12-04-183345_create_community_aggregates/up.sql +++ b/migrations/2020-12-04-183345_create_community_aggregates/up.sql @@ -2,18 +2,18 @@ create table community_aggregates ( id serial primary key, community_id int references community on update cascade on delete cascade not null, - subscribers bigint not null, - posts bigint not null, - comments bigint not null, + subscribers bigint not null default 0, + posts bigint not null default 0, + comments bigint not null default 0, unique (community_id) ); insert into community_aggregates (community_id, subscribers, posts, comments) select c.id, - coalesce(cf.subs, 0::bigint) as subscribers, - coalesce(cd.posts, 0::bigint) as posts, - coalesce(cd.comments, 0::bigint) as comments + coalesce(cf.subs, 0) as subscribers, + coalesce(cd.posts, 0) as posts, + coalesce(cd.comments, 0) as comments from community c left join ( select @@ -33,6 +33,24 @@ insert into community_aggregates (community_id, subscribers, posts, comments) ) cf on cf.community_id = c.id; -- Add community aggregate triggers + +-- initial community add +create function community_aggregates_community() +returns trigger language plpgsql +as $$ +begin + IF (TG_OP = 'INSERT') THEN + insert into community_aggregates (community_id) values (NEW.id); + ELSIF (TG_OP = 'DELETE') THEN + delete from community_aggregates where community_id = OLD.id; + END IF; + return null; +end $$; + +create trigger community_aggregates_community +after insert or delete on community +for each row +execute procedure community_aggregates_community(); -- post count create function community_aggregates_post_count() returns trigger language plpgsql @@ -44,6 +62,22 @@ begin ELSIF (TG_OP = 'DELETE') THEN update community_aggregates set posts = posts - 1 where community_id = OLD.community_id; + + -- Update the counts if the post got deleted + update community_aggregates ca + set posts = coalesce(cd.posts, 0), + comments = coalesce(cd.comments, 0) + from ( + select + c.id, + count(distinct p.id) as posts, + count(distinct ct.id) as comments + from community c + left join post p on c.id = p.community_id + left join comment ct on p.id = ct.post_id + group by c.id + ) cd + where ca.community_id = OLD.community_id; END IF; return null; end $$; @@ -59,11 +93,18 @@ returns trigger language plpgsql as $$ begin IF (TG_OP = 'INSERT') THEN - update community_aggregates - set comments = comments + 1 from comment c join post p on p.id = c.post_id and p.id = NEW.post_id; + update community_aggregates ca + set comments = comments + 1 from comment c, post p + where p.id = c.post_id + and p.id = NEW.post_id + and ca.community_id = p.community_id; ELSIF (TG_OP = 'DELETE') THEN - update community_aggregates - set comments = comments - 1 from comment c join post p on p.id = c.post_id and p.id = OLD.post_id; + update community_aggregates ca + set comments = comments - 1 from comment c, post p + where p.id = c.post_id + and p.id = OLD.post_id + and ca.community_id = p.community_id; + END IF; return null; end $$;