.unwrap();
assert_eq!(2, after_follow_again.subscribers);
- // Remove a parent comment (the comment count should also be 0)
+ // Remove a parent post (the comment count should also be 0)
Post::delete(pool, inserted_post.id).await.unwrap();
let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id)
.await
use crate::{
aggregates::post_aggregates::PostAggregates,
source::{
- comment::{Comment, CommentInsertForm},
+ comment::{Comment, CommentInsertForm, CommentUpdateForm},
community::{Community, CommunityInsertForm},
instance::Instance,
person::{Person, PersonInsertForm},
Instance::delete(pool, inserted_instance.id).await.unwrap();
}
+
+ #[tokio::test]
+ #[serial]
+ async fn test_soft_delete() {
+ let pool = &build_db_pool_for_tests().await;
+
+ let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
+ .await
+ .unwrap();
+
+ let new_person = PersonInsertForm::builder()
+ .name("thommy_community_agg".into())
+ .public_key("pubkey".to_string())
+ .instance_id(inserted_instance.id)
+ .build();
+
+ let inserted_person = Person::create(pool, &new_person).await.unwrap();
+
+ let new_community = CommunityInsertForm::builder()
+ .name("TIL_community_agg".into())
+ .title("nada".to_owned())
+ .public_key("pubkey".to_string())
+ .instance_id(inserted_instance.id)
+ .build();
+
+ let inserted_community = Community::create(pool, &new_community).await.unwrap();
+
+ let new_post = PostInsertForm::builder()
+ .name("A test post".into())
+ .creator_id(inserted_person.id)
+ .community_id(inserted_community.id)
+ .build();
+
+ let inserted_post = Post::create(pool, &new_post).await.unwrap();
+
+ let comment_form = CommentInsertForm::builder()
+ .content("A test comment".into())
+ .creator_id(inserted_person.id)
+ .post_id(inserted_post.id)
+ .build();
+
+ let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
+
+ let post_aggregates_before = PostAggregates::read(pool, inserted_post.id).await.unwrap();
+ assert_eq!(1, post_aggregates_before.comments);
+
+ Comment::update(
+ pool,
+ inserted_comment.id,
+ &CommentUpdateForm::builder().removed(Some(true)).build(),
+ )
+ .await
+ .unwrap();
+
+ let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id).await.unwrap();
+ assert_eq!(0, post_aggregates_after_remove.comments);
+
+ Comment::update(
+ pool,
+ inserted_comment.id,
+ &CommentUpdateForm::builder().removed(Some(false)).build(),
+ )
+ .await
+ .unwrap();
+
+ Comment::update(
+ pool,
+ inserted_comment.id,
+ &CommentUpdateForm::builder().deleted(Some(true)).build(),
+ )
+ .await
+ .unwrap();
+
+ let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap();
+ assert_eq!(0, post_aggregates_after_delete.comments);
+
+ Comment::update(
+ pool,
+ inserted_comment.id,
+ &CommentUpdateForm::builder().removed(Some(true)).build(),
+ )
+ .await
+ .unwrap();
+
+ let post_aggregates_after_delete_remove =
+ PostAggregates::read(pool, inserted_post.id).await.unwrap();
+ assert_eq!(0, post_aggregates_after_delete_remove.comments);
+
+ Comment::delete(pool, inserted_comment.id).await.unwrap();
+ Post::delete(pool, inserted_post.id).await.unwrap();
+ Person::delete(pool, inserted_person.id).await.unwrap();
+ Community::delete(pool, inserted_community.id)
+ .await
+ .unwrap();
+ Instance::delete(pool, inserted_instance.id).await.unwrap();
+ }
}
aggregates::site_aggregates::SiteAggregates,
source::{
comment::{Comment, CommentInsertForm},
- community::{Community, CommunityInsertForm},
+ community::{Community, CommunityInsertForm, CommunityUpdateForm},
instance::Instance,
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm},
site::{Site, SiteInsertForm},
},
traits::Crud,
- utils::build_db_pool_for_tests,
+ utils::{build_db_pool_for_tests, DbPool},
};
use serial_test::serial;
- #[tokio::test]
- #[serial]
- async fn test_crud() {
- let pool = &build_db_pool_for_tests().await;
-
+ async fn prepare_site_with_community(pool: &DbPool) -> (Instance, Person, Site, Community) {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
.build();
let inserted_community = Community::create(pool, &new_community).await.unwrap();
+ (
+ inserted_instance,
+ inserted_person,
+ inserted_site,
+ inserted_community,
+ )
+ }
+
+ #[tokio::test]
+ #[serial]
+ async fn test_crud() {
+ let pool = &build_db_pool_for_tests().await;
+
+ let (inserted_instance, inserted_person, inserted_site, inserted_community) =
+ prepare_site_with_community(pool).await;
let new_post = PostInsertForm::builder()
.name("A test post".into())
Instance::delete(pool, inserted_instance.id).await.unwrap();
}
+
+ #[tokio::test]
+ #[serial]
+ async fn test_soft_delete() {
+ let pool = &build_db_pool_for_tests().await;
+
+ let (inserted_instance, inserted_person, inserted_site, inserted_community) =
+ prepare_site_with_community(pool).await;
+
+ let site_aggregates_before = SiteAggregates::read(pool).await.unwrap();
+ assert_eq!(1, site_aggregates_before.communities);
+
+ Community::update(
+ pool,
+ inserted_community.id,
+ &CommunityUpdateForm::builder().deleted(Some(true)).build(),
+ )
+ .await
+ .unwrap();
+
+ let site_aggregates_after_delete = SiteAggregates::read(pool).await.unwrap();
+ assert_eq!(0, site_aggregates_after_delete.communities);
+
+ Community::update(
+ pool,
+ inserted_community.id,
+ &CommunityUpdateForm::builder().deleted(Some(false)).build(),
+ )
+ .await
+ .unwrap();
+
+ Community::update(
+ pool,
+ inserted_community.id,
+ &CommunityUpdateForm::builder().removed(Some(true)).build(),
+ )
+ .await
+ .unwrap();
+
+ let site_aggregates_after_remove = SiteAggregates::read(pool).await.unwrap();
+ assert_eq!(0, site_aggregates_after_remove.communities);
+
+ Community::update(
+ pool,
+ inserted_community.id,
+ &CommunityUpdateForm::builder().deleted(Some(true)).build(),
+ )
+ .await
+ .unwrap();
+
+ let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await.unwrap();
+ assert_eq!(0, site_aggregates_after_remove_delete.communities);
+
+ Community::delete(pool, inserted_community.id)
+ .await
+ .unwrap();
+ Site::delete(pool, inserted_site.id).await.unwrap();
+ Person::delete(pool, inserted_person.id).await.unwrap();
+ Instance::delete(pool, inserted_instance.id).await.unwrap();
+ }
}
--- /dev/null
+-- 2023-06-19-120700_no_double_deletion/up.sql
+create or replace function was_removed_or_deleted(TG_OP text, OLD record, NEW record)
+RETURNS boolean
+LANGUAGE plpgsql
+as $$
+ begin
+ IF (TG_OP = 'INSERT') THEN
+ return false;
+ end if;
+
+ IF (TG_OP = 'DELETE' AND OLD.deleted = 'f' AND OLD.removed = 'f') THEN
+ return true;
+ end if;
+
+ return TG_OP = 'UPDATE' AND (
+ (OLD.deleted = 'f' AND NEW.deleted = 't') OR
+ (OLD.removed = 'f' AND NEW.removed = 't')
+ );
+END $$;
+
+-- 2022-04-04-183652_update_community_aggregates_on_soft_delete/up.sql
+create or replace function was_restored_or_created(TG_OP text, OLD record, NEW record)
+ RETURNS boolean
+ LANGUAGE plpgsql
+as $$
+begin
+ IF (TG_OP = 'DELETE') THEN
+ return false;
+ end if;
+
+ IF (TG_OP = 'INSERT') THEN
+ return true;
+ end if;
+
+ return TG_OP = 'UPDATE' AND (
+ (OLD.deleted = 't' AND NEW.deleted = 'f') OR
+ (OLD.removed = 't' AND NEW.removed = 'f')
+ );
+END $$;
+
+-- 2021-08-02-002342_comment_count_fixes/up.sql
+create or replace function post_aggregates_comment_deleted()
+returns trigger language plpgsql
+as $$
+begin
+ IF NEW.deleted = TRUE THEN
+ update post_aggregates pa
+ set comments = comments - 1
+ where pa.post_id = NEW.post_id;
+ ELSE
+ update post_aggregates pa
+ set comments = comments + 1
+ where pa.post_id = NEW.post_id;
+ END IF;
+ return null;
+end $$;
+
+create trigger post_aggregates_comment_set_deleted
+after update of deleted on comment
+for each row
+execute procedure post_aggregates_comment_deleted();
+
+create or replace function post_aggregates_comment_count()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update post_aggregates pa
+ set comments = comments + 1,
+ newest_comment_time = NEW.published
+ where pa.post_id = NEW.post_id;
+
+ -- A 2 day necro-bump limit
+ update post_aggregates pa
+ set newest_comment_time_necro = NEW.published
+ from post p
+ where pa.post_id = p.id
+ and pa.post_id = NEW.post_id
+ -- Fix issue with being able to necro-bump your own post
+ and NEW.creator_id != p.creator_id
+ and pa.published > ('now'::timestamp - '2 days'::interval);
+
+ ELSIF (TG_OP = 'DELETE') THEN
+ -- Join to post because that post may not exist anymore
+ update post_aggregates pa
+ set comments = comments - 1
+ from post p
+ where pa.post_id = p.id
+ and pa.post_id = OLD.post_id;
+ ELSIF (TG_OP = 'UPDATE') THEN
+ -- Join to post because that post may not exist anymore
+ update post_aggregates pa
+ set comments = comments - 1
+ from post p
+ where pa.post_id = p.id
+ and pa.post_id = OLD.post_id;
+ END IF;
+ return null;
+end $$;
+
+-- 2020-12-10-152350_create_post_aggregates/up.sql
+create or replace trigger post_aggregates_comment_count
+after insert or delete on comment
+for each row
+execute procedure post_aggregates_comment_count();
--- /dev/null
+-- Fix for duplicated decrementations when both `deleted` and `removed` fields are set subsequently
+create or replace function was_removed_or_deleted(TG_OP text, OLD record, NEW record)
+RETURNS boolean
+LANGUAGE plpgsql
+as $$
+ begin
+ IF (TG_OP = 'INSERT') THEN
+ return false;
+ end if;
+
+ IF (TG_OP = 'DELETE' AND OLD.deleted = 'f' AND OLD.removed = 'f') THEN
+ return true;
+ end if;
+
+ return TG_OP = 'UPDATE' AND OLD.deleted = 'f' AND OLD.removed = 'f' AND (
+ NEW.deleted = 't' OR NEW.removed = 't'
+ );
+END $$;
+
+create or replace function was_restored_or_created(TG_OP text, OLD record, NEW record)
+ RETURNS boolean
+ LANGUAGE plpgsql
+as $$
+begin
+ IF (TG_OP = 'DELETE') THEN
+ return false;
+ end if;
+
+ IF (TG_OP = 'INSERT') THEN
+ return true;
+ end if;
+
+ return TG_OP = 'UPDATE' AND NEW.deleted = 'f' AND NEW.removed = 'f' AND (
+ OLD.deleted = 't' OR OLD.removed = 't'
+ );
+END $$;
+
+-- Fix for post's comment count not updating after setting `removed` to 't'
+drop trigger if exists post_aggregates_comment_set_deleted on comment;
+drop function post_aggregates_comment_deleted();
+
+create or replace function post_aggregates_comment_count()
+ returns trigger language plpgsql
+as $$
+begin
+ -- Check for post existence - it may not exist anymore
+ IF TG_OP = 'INSERT' OR EXISTS (
+ select 1 from post p where p.id = OLD.post_id
+ ) THEN
+ IF (was_restored_or_created(TG_OP, OLD, NEW)) THEN
+ update post_aggregates pa
+ set comments = comments + 1 where pa.post_id = NEW.post_id;
+ ELSIF (was_removed_or_deleted(TG_OP, OLD, NEW)) THEN
+ update post_aggregates pa
+ set comments = comments - 1 where pa.post_id = OLD.post_id;
+ END IF;
+ END IF;
+
+ IF TG_OP = 'INSERT' THEN
+ update post_aggregates pa
+ set newest_comment_time = NEW.published
+ where pa.post_id = NEW.post_id;
+
+ -- A 2 day necro-bump limit
+ update post_aggregates pa
+ set newest_comment_time_necro = NEW.published
+ from post p
+ where pa.post_id = p.id
+ and pa.post_id = NEW.post_id
+ -- Fix issue with being able to necro-bump your own post
+ and NEW.creator_id != p.creator_id
+ and pa.published > ('now'::timestamp - '2 days'::interval);
+ END IF;
+
+ return null;
+end $$;
+
+create or replace trigger post_aggregates_comment_count
+ after insert or delete or update of removed, deleted on comment
+ for each row
+execute procedure post_aggregates_comment_count();
\ No newline at end of file