mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-01 22:01:49 +00:00
Two bugs were found and fixed: - previously elements removal and deletion were counted as two separate disappearances - removing comments did not affect post aggregations
This commit is contained in:
parent
651f2747ee
commit
9c2490d4f2
5 changed files with 362 additions and 9 deletions
|
@ -167,7 +167,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(2, after_follow_again.subscribers);
|
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();
|
Post::delete(pool, inserted_post.id).await.unwrap();
|
||||||
let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id)
|
let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -38,7 +38,7 @@ mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
aggregates::post_aggregates::PostAggregates,
|
aggregates::post_aggregates::PostAggregates,
|
||||||
source::{
|
source::{
|
||||||
comment::{Comment, CommentInsertForm},
|
comment::{Comment, CommentInsertForm, CommentUpdateForm},
|
||||||
community::{Community, CommunityInsertForm},
|
community::{Community, CommunityInsertForm},
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
person::{Person, PersonInsertForm},
|
person::{Person, PersonInsertForm},
|
||||||
|
@ -181,4 +181,100 @@ mod tests {
|
||||||
|
|
||||||
Instance::delete(pool, inserted_instance.id).await.unwrap();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,22 +19,18 @@ mod tests {
|
||||||
aggregates::site_aggregates::SiteAggregates,
|
aggregates::site_aggregates::SiteAggregates,
|
||||||
source::{
|
source::{
|
||||||
comment::{Comment, CommentInsertForm},
|
comment::{Comment, CommentInsertForm},
|
||||||
community::{Community, CommunityInsertForm},
|
community::{Community, CommunityInsertForm, CommunityUpdateForm},
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
person::{Person, PersonInsertForm},
|
person::{Person, PersonInsertForm},
|
||||||
post::{Post, PostInsertForm},
|
post::{Post, PostInsertForm},
|
||||||
site::{Site, SiteInsertForm},
|
site::{Site, SiteInsertForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
utils::build_db_pool_for_tests,
|
utils::{build_db_pool_for_tests, DbPool},
|
||||||
};
|
};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
|
||||||
#[tokio::test]
|
async fn prepare_site_with_community(pool: &DbPool) -> (Instance, Person, Site, Community) {
|
||||||
#[serial]
|
|
||||||
async fn test_crud() {
|
|
||||||
let pool = &build_db_pool_for_tests().await;
|
|
||||||
|
|
||||||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
|
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -62,6 +58,21 @@ mod tests {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let inserted_community = Community::create(pool, &new_community).await.unwrap();
|
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()
|
let new_post = PostInsertForm::builder()
|
||||||
.name("A test post".into())
|
.name("A test post".into())
|
||||||
|
@ -127,4 +138,64 @@ mod tests {
|
||||||
|
|
||||||
Instance::delete(pool, inserted_instance.id).await.unwrap();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
105
migrations/2023-07-08-101154_fix_soft_delete_aggregates/down.sql
Normal file
105
migrations/2023-07-08-101154_fix_soft_delete_aggregates/down.sql
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
-- 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();
|
|
@ -0,0 +1,81 @@
|
||||||
|
-- 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();
|
Loading…
Reference in a new issue