diff --git a/Cargo.lock b/Cargo.lock index edf237ab3..caef131ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1281,6 +1281,15 @@ dependencies = [ "tokio-postgres", ] +[[package]] +name = "diesel-bind-if-some" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed8ce9db476124d2eaf4c9db45dc6581b8e8c4c4d47d5e0f39de1fb55dfb2a7" +dependencies = [ + "diesel", +] + [[package]] name = "diesel-derive-enum" version = "2.1.0" @@ -2613,6 +2622,7 @@ dependencies = [ "derive-new", "diesel", "diesel-async", + "diesel-bind-if-some", "diesel-derive-enum", "diesel-derive-newtype", "diesel_ltree", @@ -2634,6 +2644,7 @@ dependencies = [ "tokio-postgres-rustls", "tracing", "ts-rs", + "tuplex", "url", "uuid", ] @@ -5272,6 +5283,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "tuplex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "676ac81d5454c4dcf37955d34fa8626ede3490f744b86ca14a7b90168d2a08aa" + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 8db7b8b8d..807e24e3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,6 +160,8 @@ i-love-jesus = { version = "0.1.0" } clap = { version = "4.5.13", features = ["derive", "env"] } pretty_assertions = "1.4.0" derive-new = "0.7.0" +diesel-bind-if-some = "0.1.0" +tuplex = "0.1.2" [dependencies] lemmy_api = { workspace = true } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 5d6d540c9..09cdac28c 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -153,7 +153,6 @@ pub async fn update_read_comments( person_id, post_id, read_comments, - ..PersonPostAggregatesForm::default() }; PersonPostAggregates::upsert(pool, &person_post_agg_form).await?; diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index efa2c5247..2a9abc939 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -194,10 +194,16 @@ impl Object for ApubCommunity { LanguageTag::to_language_id_multiple(group.language, &mut context.pool()).await?; let timestamp = group.updated.or(group.published).unwrap_or_else(naive_now); - let community = Community::insert_apub(&mut context.pool(), timestamp, &form).await?; + let community: ApubCommunity = Community::insert_apub(&mut context.pool(), timestamp, &form) + .await? + .into(); CommunityLanguage::update(&mut context.pool(), languages, community.id).await?; - let community: ApubCommunity = community.into(); + // Need to fetch mods synchronously, otherwise fetching a post in community with + // `posting_restricted_to_mods` can fail if mods havent been fetched yet. + if let Some(moderators) = group.attributed_to { + moderators.dereference(&community, context).await.ok(); + } // These collections are not necessary for Lemmy to work, so ignore errors. let community_ = community.clone(); @@ -210,9 +216,6 @@ impl Object for ApubCommunity { if let Some(featured) = group.featured { featured.dereference(&community_, &context_).await.ok(); } - if let Some(moderators) = group.attributed_to { - moderators.dereference(&community_, &context_).await.ok(); - } Ok(()) }); diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index c9b2a7930..c52629ce3 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -37,6 +37,8 @@ full = [ "tokio-postgres-rustls", "rustls", "i-love-jesus", + "tuplex", + "diesel-bind-if-some", ] [dependencies] @@ -76,8 +78,10 @@ rustls = { workspace = true, optional = true } uuid = { workspace = true, features = ["v4"] } i-love-jesus = { workspace = true, optional = true } anyhow = { workspace = true } +diesel-bind-if-some = { workspace = true, optional = true } moka.workspace = true derive-new.workspace = true +tuplex = { workspace = true, optional = true } [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 973d3325f..6c55ce3d6 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -38,7 +38,7 @@ AS $a$ BEGIN EXECUTE replace($b$ -- When a thing gets a vote, update its aggregates and its creator's aggregates - CALL r.create_triggers ('thing_like', $$ + CALL r.create_triggers ('thing_actions', $$ BEGIN WITH thing_diff AS ( UPDATE thing_aggregates AS a @@ -46,7 +46,8 @@ BEGIN score = a.score + diff.upvotes - diff.downvotes, upvotes = a.upvotes + diff.upvotes, downvotes = a.downvotes + diff.downvotes, controversy_rank = r.controversy_rank ((a.upvotes + diff.upvotes)::numeric, (a.downvotes + diff.downvotes)::numeric) FROM ( SELECT - (thing_like).thing_id, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score = 1), 0) AS upvotes, coalesce(sum(count_diff) FILTER (WHERE (thing_like).score != 1), 0) AS downvotes FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (thing_like).thing_id) AS diff + (thing_actions).thing_id, coalesce(sum(count_diff) FILTER (WHERE (thing_actions).like_score = 1), 0) AS upvotes, coalesce(sum(count_diff) FILTER (WHERE (thing_actions).like_score != 1), 0) AS downvotes FROM select_old_and_new_rows AS old_and_new_rows + WHERE (thing_actions).like_score IS NOT NULL GROUP BY (thing_actions).thing_id) AS diff WHERE a.thing_id = diff.thing_id AND (diff.upvotes, diff.downvotes) != (0, 0) @@ -360,7 +361,7 @@ CREATE TRIGGER comment_count -- Count subscribers for communities. -- subscribers should be updated only when a local community is followed by a local or remote person. -- subscribers_local should be updated only when a local person follows a local or remote community. -CALL r.create_triggers ('community_follower', $$ +CALL r.create_triggers ('community_actions', $$ BEGIN UPDATE community_aggregates AS a @@ -368,10 +369,11 @@ BEGIN subscribers = a.subscribers + diff.subscribers, subscribers_local = a.subscribers_local + diff.subscribers_local FROM ( SELECT - (community_follower).community_id, coalesce(sum(count_diff) FILTER (WHERE community.local), 0) AS subscribers, coalesce(sum(count_diff) FILTER (WHERE person.local), 0) AS subscribers_local + (community_actions).community_id, coalesce(sum(count_diff) FILTER (WHERE community.local), 0) AS subscribers, coalesce(sum(count_diff) FILTER (WHERE person.local), 0) AS subscribers_local FROM select_old_and_new_rows AS old_and_new_rows - LEFT JOIN community ON community.id = (community_follower).community_id - LEFT JOIN person ON person.id = (community_follower).person_id GROUP BY (community_follower).community_id) AS diff + LEFT JOIN community ON community.id = (community_actions).community_id + LEFT JOIN person ON person.id = (community_actions).person_id + WHERE (community_actions).followed IS NOT NULL GROUP BY (community_actions).community_id) AS diff WHERE a.community_id = diff.community_id AND (diff.subscribers, diff.subscribers_local) != (0, 0); @@ -541,7 +543,7 @@ CREATE FUNCTION r.delete_follow_before_person () LANGUAGE plpgsql AS $$ BEGIN - DELETE FROM community_follower AS c + DELETE FROM community_actions AS c WHERE c.person_id = OLD.id; RETURN OLD; END; diff --git a/crates/db_schema/replaceable_schema/utils.sql b/crates/db_schema/replaceable_schema/utils.sql index c766d25f2..0c7f42ff2 100644 --- a/crates/db_schema/replaceable_schema/utils.sql +++ b/crates/db_schema/replaceable_schema/utils.sql @@ -151,3 +151,118 @@ DECLARE END; $a$; +-- Edit community aggregates to include voters as active users +CREATE OR REPLACE FUNCTION r.community_aggregates_activity (i text) + RETURNS TABLE ( + count_ bigint, + community_id_ integer) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN query + SELECT + count(*), + community_id + FROM ( + SELECT + c.creator_id, + p.community_id + FROM + comment c + INNER JOIN post p ON c.post_id = p.id + INNER JOIN person pe ON c.creator_id = pe.id + WHERE + c.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + p.creator_id, + p.community_id + FROM + post p + INNER JOIN person pe ON p.creator_id = pe.id + WHERE + p.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + pl.person_id, + p.community_id + FROM + post_like pl + INNER JOIN post p ON pl.post_id = p.id + INNER JOIN person pe ON pl.person_id = pe.id + WHERE + pl.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + cl.person_id, + p.community_id + FROM + comment_like cl + INNER JOIN comment c ON cl.comment_id = c.id + INNER JOIN post p ON c.post_id = p.id + INNER JOIN person pe ON cl.person_id = pe.id + WHERE + cl.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE) a +GROUP BY + community_id; +END; +$$; + +-- Edit site aggregates to include voters and people who have read posts as active users +CREATE OR REPLACE FUNCTION r.site_aggregates_activity (i text) + RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE + count_ integer; +BEGIN + SELECT + count(*) INTO count_ + FROM ( + SELECT + c.creator_id + FROM + comment c + INNER JOIN person pe ON c.creator_id = pe.id + WHERE + c.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + p.creator_id + FROM + post p + INNER JOIN person pe ON p.creator_id = pe.id + WHERE + p.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + pl.person_id + FROM + post_like pl + INNER JOIN person pe ON pl.person_id = pe.id + WHERE + pl.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + cl.person_id + FROM + comment_like cl + INNER JOIN person pe ON cl.person_id = pe.id + WHERE + cl.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE) a; + RETURN count_; +END; +$$; + diff --git a/crates/db_schema/src/aggregates/person_post_aggregates.rs b/crates/db_schema/src/aggregates/person_post_aggregates.rs index f6e108ee9..63a50af9c 100644 --- a/crates/db_schema/src/aggregates/person_post_aggregates.rs +++ b/crates/db_schema/src/aggregates/person_post_aggregates.rs @@ -2,10 +2,17 @@ use crate::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, diesel::OptionalExtension, newtypes::{PersonId, PostId}, - schema::person_post_aggregates::dsl::{person_id, person_post_aggregates, post_id}, - utils::{get_conn, DbPool}, + schema::post_actions, + utils::{find_action, get_conn, now, DbPool}, +}; +use diesel::{ + expression::SelectableHelper, + insert_into, + result::Error, + ExpressionMethods, + NullableExpressionMethods, + QueryDsl, }; -use diesel::{insert_into, result::Error, QueryDsl}; use diesel_async::RunQueryDsl; impl PersonPostAggregates { @@ -14,11 +21,13 @@ impl PersonPostAggregates { form: &PersonPostAggregatesForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_post_aggregates) + let form = (form, post_actions::read_comments.eq(now().nullable())); + insert_into(post_actions::table) .values(form) - .on_conflict((person_id, post_id)) + .on_conflict((post_actions::person_id, post_actions::post_id)) .do_update() .set(form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -28,8 +37,8 @@ impl PersonPostAggregates { post_id_: PostId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - person_post_aggregates - .find((person_id_, post_id_)) + find_action(post_actions::read_comments, (person_id_, post_id_)) + .select(Self::as_select()) .first(conn) .await .optional() diff --git a/crates/db_schema/src/aggregates/structs.rs b/crates/db_schema/src/aggregates/structs.rs index fd7f70409..c2e54ae5c 100644 --- a/crates/db_schema/src/aggregates/structs.rs +++ b/crates/db_schema/src/aggregates/structs.rs @@ -4,12 +4,14 @@ use crate::schema::{ comment_aggregates, community_aggregates, person_aggregates, - person_post_aggregates, + post_actions, post_aggregates, site_aggregates, }; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; +#[cfg(feature = "full")] use i_love_jesus::CursorKeysModule; use serde::{Deserialize, Serialize}; #[cfg(feature = "full")] @@ -151,7 +153,7 @@ pub struct PostAggregates { feature = "full", derive(Queryable, Selectable, Associations, Identifiable) )] -#[cfg_attr(feature = "full", diesel(table_name = person_post_aggregates))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] @@ -162,18 +164,22 @@ pub struct PersonPostAggregates { /// The number of comments they've read on that post. /// /// This is updated to the current post comment count every time they view a post. + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::read_comments_amount.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub read_comments: i64, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::read_comments.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone, Default)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_post_aggregates))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub struct PersonPostAggregatesForm { pub person_id: PersonId, pub post_id: PostId, + #[cfg_attr(feature = "full", diesel(column_name = read_comments_amount))] pub read_comments: i64, - pub published: Option>, } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Copy, Hash)] diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index d261dbf2c..96ec70fa2 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -1,7 +1,7 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommentId, DbUrl, PersonId}, - schema::comment, + schema::{comment, comment_actions}, source::comment::{ Comment, CommentInsertForm, @@ -12,10 +12,25 @@ use crate::{ CommentUpdateForm, }, traits::{Crud, Likeable, Saveable}, - utils::{functions::coalesce, get_conn, naive_now, DbPool, DELETED_REPLACEMENT_TEXT}, + utils::{ + functions::coalesce, + get_conn, + naive_now, + now, + uplete, + DbPool, + DELETED_REPLACEMENT_TEXT, + }, }; use chrono::{DateTime, Utc}; -use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel::{ + dsl::insert_into, + expression::SelectableHelper, + result::Error, + ExpressionMethods, + NullableExpressionMethods, + QueryDsl, +}; use diesel_async::RunQueryDsl; use diesel_ltree::Ltree; use url::Url; @@ -141,13 +156,17 @@ impl Likeable for CommentLike { type Form = CommentLikeForm; type IdType = CommentId; async fn like(pool: &mut DbPool<'_>, comment_like_form: &CommentLikeForm) -> Result { - use crate::schema::comment_like::dsl::{comment_id, comment_like, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(comment_like) + let comment_like_form = ( + comment_like_form, + comment_actions::liked.eq(now().nullable()), + ); + insert_into(comment_actions::table) .values(comment_like_form) - .on_conflict((comment_id, person_id)) + .on_conflict((comment_actions::comment_id, comment_actions::person_id)) .do_update() .set(comment_like_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -155,11 +174,12 @@ impl Likeable for CommentLike { pool: &mut DbPool<'_>, person_id: PersonId, comment_id: CommentId, - ) -> Result { - use crate::schema::comment_like::dsl::comment_like; + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(comment_like.find((person_id, comment_id))) - .execute(conn) + uplete::new(comment_actions::table.find((person_id, comment_id))) + .set_null(comment_actions::like_score) + .set_null(comment_actions::liked) + .get_result(conn) .await } } @@ -171,26 +191,30 @@ impl Saveable for CommentSaved { pool: &mut DbPool<'_>, comment_saved_form: &CommentSavedForm, ) -> Result { - use crate::schema::comment_saved::dsl::{comment_id, comment_saved, person_id}; let conn = &mut get_conn(pool).await?; - insert_into(comment_saved) + let comment_saved_form = ( + comment_saved_form, + comment_actions::saved.eq(now().nullable()), + ); + insert_into(comment_actions::table) .values(comment_saved_form) - .on_conflict((comment_id, person_id)) + .on_conflict((comment_actions::comment_id, comment_actions::person_id)) .do_update() .set(comment_saved_form) + .returning(Self::as_select()) .get_result::(conn) .await } async fn unsave( pool: &mut DbPool<'_>, comment_saved_form: &CommentSavedForm, - ) -> Result { - use crate::schema::comment_saved::dsl::comment_saved; + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - comment_saved.find((comment_saved_form.person_id, comment_saved_form.comment_id)), + uplete::new( + comment_actions::table.find((comment_saved_form.person_id, comment_saved_form.comment_id)), ) - .execute(conn) + .set_null(comment_actions::saved) + .get_result(conn) .await } } @@ -216,7 +240,7 @@ mod tests { post::{Post, PostInsertForm}, }, traits::{Crud, Likeable, Saveable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, }; use diesel_ltree::Ltree; use lemmy_utils::error::LemmyResult; @@ -342,8 +366,8 @@ mod tests { format!("0.{}.{}", expected_comment.id, inserted_child_comment.id), inserted_child_comment.path.0, ); - assert_eq!(1, like_removed); - assert_eq!(1, saved_removed); + assert_eq!(uplete::Count::only_updated(1), like_removed); + assert_eq!(uplete::Count::only_deleted(1), saved_removed); assert_eq!(1, num_deleted); Ok(()) diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 5375bcc3c..03cc12558 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -1,14 +1,7 @@ use crate::{ diesel::{DecoratableTarget, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId}, - schema::{ - community, - community_follower, - community_moderator, - community_person_ban, - instance, - post, - }, + schema::{community, community_actions, instance, post}, source::{ actor_language::CommunityLanguage, community::{ @@ -27,8 +20,12 @@ use crate::{ }, traits::{ApubActor, Bannable, Crud, Followable, Joinable}, utils::{ + action_query, + find_action, functions::{coalesce, lower}, get_conn, + now, + uplete, DbPool, }, ListingType, @@ -38,6 +35,7 @@ use chrono::{DateTime, Utc}; use diesel::{ deserialize, dsl::{self, exists, insert_into, not}, + expression::SelectableHelper, pg::Pg, result::Error, select, @@ -93,8 +91,19 @@ impl Joinable for CommunityModerator { community_moderator_form: &CommunityModeratorForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_moderator::table) + let community_moderator_form = ( + community_moderator_form, + community_actions::became_moderator.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_moderator_form) + .on_conflict(( + community_actions::person_id, + community_actions::community_id, + )) + .do_update() + .set(community_moderator_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -102,13 +111,14 @@ impl Joinable for CommunityModerator { async fn leave( pool: &mut DbPool<'_>, community_moderator_form: &CommunityModeratorForm, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_moderator::table.find(( + uplete::new(community_actions::table.find(( community_moderator_form.person_id, community_moderator_form.community_id, ))) - .execute(conn) + .set_null(community_actions::became_moderator) + .get_result(conn) .await } } @@ -225,26 +235,26 @@ impl CommunityModerator { pub async fn delete_for_community( pool: &mut DbPool<'_>, for_community_id: CommunityId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - community_moderator::table.filter(community_moderator::community_id.eq(for_community_id)), + uplete::new( + community_actions::table.filter(community_actions::community_id.eq(for_community_id)), ) - .execute(conn) + .set_null(community_actions::became_moderator) + .get_result(conn) .await } pub async fn leave_all_communities( pool: &mut DbPool<'_>, for_person_id: PersonId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - community_moderator::table.filter(community_moderator::person_id.eq(for_person_id)), - ) - .execute(conn) - .await + uplete::new(community_actions::table.filter(community_actions::person_id.eq(for_person_id))) + .set_null(community_actions::became_moderator) + .get_result(conn) + .await } pub async fn get_person_moderated_communities( @@ -252,9 +262,9 @@ impl CommunityModerator { for_person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_moderator::table - .filter(community_moderator::person_id.eq(for_person_id)) - .select(community_moderator::community_id) + action_query(community_actions::became_moderator) + .filter(community_actions::person_id.eq(for_person_id)) + .select(community_actions::community_id) .load::(conn) .await } @@ -273,16 +283,17 @@ impl CommunityModerator { persons.push(mod_person_id); persons.dedup(); - let res = community_moderator::table - .filter(community_moderator::community_id.eq(for_community_id)) - .filter(community_moderator::person_id.eq_any(persons)) - .order_by(community_moderator::published) + let res = action_query(community_actions::became_moderator) + .filter(community_actions::community_id.eq(for_community_id)) + .filter(community_actions::person_id.eq_any(persons)) + .order_by(community_actions::became_moderator) + .select(community_actions::person_id) // This does a limit 1 select first - .first::(conn) + .first::(conn) .await?; // If the first result sorted by published is the acting mod - if res.person_id == mod_person_id { + if res == mod_person_id { Ok(()) } else { Err(LemmyErrorType::NotHigherMod)? @@ -298,14 +309,19 @@ impl Bannable for CommunityPersonBan { community_person_ban_form: &CommunityPersonBanForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_person_ban::table) + let community_person_ban_form = ( + community_person_ban_form, + community_actions::received_ban.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_person_ban_form) .on_conflict(( - community_person_ban::community_id, - community_person_ban::person_id, + community_actions::community_id, + community_actions::person_id, )) .do_update() .set(community_person_ban_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -313,20 +329,22 @@ impl Bannable for CommunityPersonBan { async fn unban( pool: &mut DbPool<'_>, community_person_ban_form: &CommunityPersonBanForm, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_person_ban::table.find(( + uplete::new(community_actions::table.find(( community_person_ban_form.person_id, community_person_ban_form.community_id, ))) - .execute(conn) + .set_null(community_actions::received_ban) + .set_null(community_actions::ban_expires) + .get_result(conn) .await } } impl CommunityFollower { - pub fn select_subscribed_type() -> dsl::Nullable { - community_follower::state.nullable() + pub fn select_subscribed_type() -> dsl::Nullable { + community_actions::follow_state.nullable() } /// Check if a remote instance has any followers on local instance. For this it is enough to check @@ -336,9 +354,10 @@ impl CommunityFollower { remote_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(exists(community_follower::table.filter( - community_follower::community_id.eq(remote_community_id), - ))) + select(exists( + action_query(community_actions::followed) + .filter(community_actions::community_id.eq(remote_community_id)), + )) .get_result::(conn) .await? .then_some(()) @@ -352,13 +371,16 @@ impl CommunityFollower { approver_id: PersonId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - diesel::update(community_follower::table.find((follower_id, community_id))) - .set(( - community_follower::state.eq(CommunityFollowerState::Accepted), - community_follower::approver_id.eq(approver_id), - )) - .get_result::(conn) - .await?; + diesel::update(find_action( + community_actions::followed, + (follower_id, community_id), + )) + .set(( + community_actions::follow_state.eq(CommunityFollowerState::Accepted), + community_actions::follow_approver_id.eq(approver_id), + )) + .execute(conn) + .await?; Ok(()) } } @@ -382,14 +404,16 @@ impl Followable for CommunityFollower { type Form = CommunityFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_follower::table) + let form = (form, community_actions::followed.eq(now().nullable())); + insert_into(community_actions::table) .values(form) .on_conflict(( - community_follower::community_id, - community_follower::person_id, + community_actions::community_id, + community_actions::person_id, )) .do_update() .set(form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -399,16 +423,25 @@ impl Followable for CommunityFollower { person_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(community_follower::table.find((person_id, community_id))) - .set(community_follower::state.eq(CommunityFollowerState::Accepted)) - .get_result::(conn) - .await + diesel::update(find_action( + community_actions::follow_state, + (person_id, community_id), + )) + .set(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted))) + .returning(Self::as_select()) + .get_result::(conn) + .await } - - async fn unfollow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result { + async fn unfollow( + pool: &mut DbPool<'_>, + form: &CommunityFollowerForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_follower::table.find((form.person_id, form.community_id))) - .execute(conn) + uplete::new(community_actions::table.find((form.person_id, form.community_id))) + .set_null(community_actions::followed) + .set_null(community_actions::follow_state) + .set_null(community_actions::follow_approver_id) + .get_result(conn) .await } } @@ -483,7 +516,7 @@ mod tests { person::{Person, PersonInsertForm}, }, traits::{Bannable, Crud, Followable, Joinable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, CommunityVisibility, }; use lemmy_utils::error::LemmyResult; @@ -650,9 +683,9 @@ mod tests { assert_eq!(expected_community_follower, inserted_community_follower); assert_eq!(expected_community_moderator, inserted_bobby_moderator); assert_eq!(expected_community_person_ban, inserted_community_person_ban); - assert_eq!(1, ignored_community); - assert_eq!(1, left_community); - assert_eq!(1, unban); + assert_eq!(uplete::Count::only_updated(1), ignored_community); + assert_eq!(uplete::Count::only_updated(1), left_community); + assert_eq!(uplete::Count::only_deleted(1), unban); // assert_eq!(2, loaded_count); assert_eq!(1, num_deleted); diff --git a/crates/db_schema/src/impls/community_block.rs b/crates/db_schema/src/impls/community_block.rs index c78953d27..c520e43e8 100644 --- a/crates/db_schema/src/impls/community_block.rs +++ b/crates/db_schema/src/impls/community_block.rs @@ -1,18 +1,20 @@ use crate::{ newtypes::{CommunityId, PersonId}, - schema::{community, community_block}, + schema::{community, community_actions}, source::{ community::Community, community_block::{CommunityBlock, CommunityBlockForm}, }, traits::Blockable, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, ExpressionMethods, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -25,9 +27,10 @@ impl CommunityBlock { for_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - community_block::table.find((for_person_id, for_community_id)), - ))) + select(not(exists(find_action( + community_actions::blocked, + (for_person_id, for_community_id), + )))) .get_result::(conn) .await? .then_some(()) @@ -39,13 +42,13 @@ impl CommunityBlock { person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_block::table + action_query(community_actions::blocked) .inner_join(community::table) .select(community::all_columns) - .filter(community_block::person_id.eq(person_id)) + .filter(community_actions::person_id.eq(person_id)) .filter(community::deleted.eq(false)) .filter(community::removed.eq(false)) - .order_by(community_block::published) + .order_by(community_actions::blocked) .load::(conn) .await } @@ -56,24 +59,33 @@ impl Blockable for CommunityBlock { type Form = CommunityBlockForm; async fn block(pool: &mut DbPool<'_>, community_block_form: &Self::Form) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_block::table) + let community_block_form = ( + community_block_form, + community_actions::blocked.eq(now().nullable()), + ); + insert_into(community_actions::table) .values(community_block_form) - .on_conflict((community_block::person_id, community_block::community_id)) + .on_conflict(( + community_actions::person_id, + community_actions::community_id, + )) .do_update() .set(community_block_form) + .returning(Self::as_select()) .get_result::(conn) .await } async fn unblock( pool: &mut DbPool<'_>, community_block_form: &Self::Form, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(community_block::table.find(( + uplete::new(community_actions::table.find(( community_block_form.person_id, community_block_form.community_id, ))) - .execute(conn) + .set_null(community_actions::blocked) + .get_result(conn) .await } } diff --git a/crates/db_schema/src/impls/instance_block.rs b/crates/db_schema/src/impls/instance_block.rs index 1b70f0e08..1722e8318 100644 --- a/crates/db_schema/src/impls/instance_block.rs +++ b/crates/db_schema/src/impls/instance_block.rs @@ -1,18 +1,20 @@ use crate::{ newtypes::{InstanceId, PersonId}, - schema::{instance, instance_block}, + schema::{instance, instance_actions}, source::{ instance::Instance, instance_block::{InstanceBlock, InstanceBlockForm}, }, traits::Blockable, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, ExpressionMethods, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -25,9 +27,10 @@ impl InstanceBlock { for_instance_id: InstanceId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - instance_block::table.find((for_person_id, for_instance_id)), - ))) + select(not(exists(find_action( + instance_actions::blocked, + (for_person_id, for_instance_id), + )))) .get_result::(conn) .await? .then_some(()) @@ -39,11 +42,11 @@ impl InstanceBlock { person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - instance_block::table + action_query(instance_actions::blocked) .inner_join(instance::table) .select(instance::all_columns) - .filter(instance_block::person_id.eq(person_id)) - .order_by(instance_block::published) + .filter(instance_actions::person_id.eq(person_id)) + .order_by(instance_actions::blocked) .load::(conn) .await } @@ -54,24 +57,30 @@ impl Blockable for InstanceBlock { type Form = InstanceBlockForm; async fn block(pool: &mut DbPool<'_>, instance_block_form: &Self::Form) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(instance_block::table) + let instance_block_form = ( + instance_block_form, + instance_actions::blocked.eq(now().nullable()), + ); + insert_into(instance_actions::table) .values(instance_block_form) - .on_conflict((instance_block::person_id, instance_block::instance_id)) + .on_conflict((instance_actions::person_id, instance_actions::instance_id)) .do_update() .set(instance_block_form) + .returning(Self::as_select()) .get_result::(conn) .await } async fn unblock( pool: &mut DbPool<'_>, instance_block_form: &Self::Form, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(instance_block::table.find(( + uplete::new(instance_actions::table.find(( instance_block_form.person_id, instance_block_form.instance_id, ))) - .execute(conn) + .set_null(instance_actions::blocked) + .get_result(conn) .await } } diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 69a5ef314..3b695a97e 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{CommunityId, DbUrl, LanguageId, LocalUserId, PersonId}, - schema::{community, community_moderator, local_user, person, registration_application}, + schema::{community, community_actions, local_user, person, registration_application}, source::{ actor_language::LocalUserLanguage, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, @@ -8,6 +8,7 @@ use crate::{ site::Site, }, utils::{ + action_query, functions::{coalesce, lower}, get_conn, now, @@ -155,55 +156,54 @@ impl LocalUser { ) -> Result { use crate::schema::{ comment, - comment_saved, + comment_actions, community, - community_block, - community_follower, + community_actions, instance, - instance_block, - person_block, + instance_actions, + person_actions, post, - post_saved, + post_actions, }; let conn = &mut get_conn(pool).await?; - let followed_communities = community_follower::dsl::community_follower - .filter(community_follower::person_id.eq(person_id_)) - .inner_join(community::table.on(community_follower::community_id.eq(community::id))) - .select(community::actor_id) - .get_results(conn) - .await?; - - let saved_posts = post_saved::dsl::post_saved - .filter(post_saved::person_id.eq(person_id_)) - .inner_join(post::table.on(post_saved::post_id.eq(post::id))) - .select(post::ap_id) - .get_results(conn) - .await?; - - let saved_comments = comment_saved::dsl::comment_saved - .filter(comment_saved::person_id.eq(person_id_)) - .inner_join(comment::table.on(comment_saved::comment_id.eq(comment::id))) - .select(comment::ap_id) - .get_results(conn) - .await?; - - let blocked_communities = community_block::dsl::community_block - .filter(community_block::person_id.eq(person_id_)) + let followed_communities = action_query(community_actions::followed) + .filter(community_actions::person_id.eq(person_id_)) .inner_join(community::table) .select(community::actor_id) .get_results(conn) .await?; - let blocked_users = person_block::dsl::person_block - .filter(person_block::person_id.eq(person_id_)) - .inner_join(person::table.on(person_block::target_id.eq(person::id))) + let saved_posts = action_query(post_actions::saved) + .filter(post_actions::person_id.eq(person_id_)) + .inner_join(post::table) + .select(post::ap_id) + .get_results(conn) + .await?; + + let saved_comments = action_query(comment_actions::saved) + .filter(comment_actions::person_id.eq(person_id_)) + .inner_join(comment::table) + .select(comment::ap_id) + .get_results(conn) + .await?; + + let blocked_communities = action_query(community_actions::blocked) + .filter(community_actions::person_id.eq(person_id_)) + .inner_join(community::table) + .select(community::actor_id) + .get_results(conn) + .await?; + + let blocked_users = action_query(person_actions::blocked) + .filter(person_actions::person_id.eq(person_id_)) + .inner_join(person::table.on(person_actions::target_id.eq(person::id))) .select(person::actor_id) .get_results(conn) .await?; - let blocked_instances = instance_block::dsl::instance_block - .filter(instance_block::person_id.eq(person_id_)) + let blocked_instances = action_query(instance_actions::blocked) + .filter(instance_actions::person_id.eq(person_id_)) .inner_join(instance::table) .select(instance::domain) .get_results(conn) @@ -270,11 +270,11 @@ impl LocalUser { .order_by(local_user::id) .select(local_user::person_id); - let mods = community_moderator::table - .filter(community_moderator::community_id.eq(for_community_id)) - .filter(community_moderator::person_id.eq_any(&persons)) - .order_by(community_moderator::published) - .select(community_moderator::person_id); + let mods = action_query(community_actions::became_moderator) + .filter(community_actions::community_id.eq(for_community_id)) + .filter(community_actions::person_id.eq_any(&persons)) + .order_by(community_actions::became_moderator) + .select(community_actions::person_id); let res = admins.union_all(mods).get_results::(conn).await?; let first_person = res.as_slice().first().ok_or(LemmyErrorType::NotHigherMod)?; diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index 85ab20d6a..3ae355b87 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -1,7 +1,7 @@ use crate::{ diesel::OptionalExtension, newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, - schema::{comment, community, instance, local_user, person, person_follower, post}, + schema::{comment, community, instance, local_user, person, person_actions, post}, source::person::{ Person, PersonFollower, @@ -10,14 +10,16 @@ use crate::{ PersonUpdateForm, }, traits::{ApubActor, Crud, Followable}, - utils::{functions::lower, get_conn, naive_now, DbPool}, + utils::{action_query, functions::lower, get_conn, naive_now, now, uplete, DbPool}, }; use diesel::{ dsl::{insert_into, not}, + expression::SelectableHelper, result::Error, CombineDsl, ExpressionMethods, JoinOnDsl, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -197,11 +199,13 @@ impl Followable for PersonFollower { type Form = PersonFollowerForm; async fn follow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_follower::table) + let form = (form, person_actions::followed.eq(now().nullable())); + insert_into(person_actions::table) .values(form) - .on_conflict((person_follower::follower_id, person_follower::person_id)) + .on_conflict((person_actions::person_id, person_actions::target_id)) .do_update() .set(form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -211,10 +215,15 @@ impl Followable for PersonFollower { Err(Error::NotFound) } - async fn unfollow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result { + async fn unfollow( + pool: &mut DbPool<'_>, + form: &PersonFollowerForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(person_follower::table.find((form.follower_id, form.person_id))) - .execute(conn) + uplete::new(person_actions::table.find((form.follower_id, form.person_id))) + .set_null(person_actions::followed) + .set_null(person_actions::follow_pending) + .get_result(conn) .await } } @@ -225,9 +234,9 @@ impl PersonFollower { for_person_id: PersonId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - person_follower::table - .inner_join(person::table.on(person_follower::follower_id.eq(person::id))) - .filter(person_follower::person_id.eq(for_person_id)) + action_query(person_actions::followed) + .inner_join(person::table.on(person_actions::person_id.eq(person::id))) + .filter(person_actions::target_id.eq(for_person_id)) .select(person::all_columns) .load(conn) .await @@ -243,7 +252,7 @@ mod tests { person::{Person, PersonFollower, PersonFollowerForm, PersonInsertForm, PersonUpdateForm}, }, traits::{Crud, Followable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, }; use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; @@ -329,7 +338,7 @@ mod tests { assert_eq!(vec![person_2], followers); let unfollow = PersonFollower::unfollow(pool, &follow_form).await?; - assert_eq!(1, unfollow); + assert_eq!(uplete::Count::only_deleted(1), unfollow); Ok(()) } diff --git a/crates/db_schema/src/impls/person_block.rs b/crates/db_schema/src/impls/person_block.rs index 44c83b3f8..363a2d3d1 100644 --- a/crates/db_schema/src/impls/person_block.rs +++ b/crates/db_schema/src/impls/person_block.rs @@ -1,19 +1,21 @@ use crate::{ newtypes::PersonId, - schema::{person, person_block}, + schema::{person, person_actions}, source::{ person::Person, person_block::{PersonBlock, PersonBlockForm}, }, traits::Blockable, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, now, uplete, DbPool}, }; use diesel::{ dsl::{exists, insert_into, not}, + expression::SelectableHelper, result::Error, select, ExpressionMethods, JoinOnDsl, + NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; @@ -26,9 +28,10 @@ impl PersonBlock { for_recipient_id: PersonId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - person_block::table.find((for_person_id, for_recipient_id)), - ))) + select(not(exists(find_action( + person_actions::blocked, + (for_person_id, for_recipient_id), + )))) .get_result::(conn) .await? .then_some(()) @@ -42,15 +45,15 @@ impl PersonBlock { let conn = &mut get_conn(pool).await?; let target_person_alias = diesel::alias!(person as person1); - person_block::table - .inner_join(person::table.on(person_block::person_id.eq(person::id))) + action_query(person_actions::blocked) + .inner_join(person::table.on(person_actions::person_id.eq(person::id))) .inner_join( - target_person_alias.on(person_block::target_id.eq(target_person_alias.field(person::id))), + target_person_alias.on(person_actions::target_id.eq(target_person_alias.field(person::id))), ) .select(target_person_alias.fields(person::all_columns)) - .filter(person_block::person_id.eq(person_id)) + .filter(person_actions::person_id.eq(person_id)) .filter(target_person_alias.field(person::deleted).eq(false)) - .order_by(person_block::published) + .order_by(person_actions::blocked) .load::(conn) .await } @@ -64,20 +67,29 @@ impl Blockable for PersonBlock { person_block_form: &PersonBlockForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(person_block::table) + let person_block_form = ( + person_block_form, + person_actions::blocked.eq(now().nullable()), + ); + insert_into(person_actions::table) .values(person_block_form) - .on_conflict((person_block::person_id, person_block::target_id)) + .on_conflict((person_actions::person_id, person_actions::target_id)) .do_update() .set(person_block_form) + .returning(Self::as_select()) .get_result::(conn) .await } - async fn unblock(pool: &mut DbPool<'_>, person_block_form: &Self::Form) -> Result { + async fn unblock( + pool: &mut DbPool<'_>, + person_block_form: &Self::Form, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - person_block::table.find((person_block_form.person_id, person_block_form.target_id)), + uplete::new( + person_actions::table.find((person_block_form.person_id, person_block_form.target_id)), ) - .execute(conn) + .set_null(person_actions::blocked) + .get_result(conn) .await } } diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index a7d32503d..0c69b25ae 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -1,7 +1,7 @@ use crate::{ diesel::{BoolExpressionMethods, OptionalExtension}, newtypes::{CommunityId, DbUrl, PersonId, PostId}, - schema::{community, person, post, post_hide, post_like, post_read, post_saved}, + schema::{community, person, post, post_actions}, source::post::{ Post, PostHide, @@ -21,6 +21,7 @@ use crate::{ get_conn, naive_now, now, + uplete, DbPool, DELETED_REPLACEMENT_TEXT, FETCH_LIMIT_MAX, @@ -32,9 +33,11 @@ use ::url::Url; use chrono::{DateTime, Utc}; use diesel::{ dsl::{count, insert_into, not}, + expression::SelectableHelper, result::Error, DecoratableTarget, ExpressionMethods, + NullableExpressionMethods, QueryDsl, TextExpressionMethods, }; @@ -279,11 +282,13 @@ impl Likeable for PostLike { type IdType = PostId; async fn like(pool: &mut DbPool<'_>, post_like_form: &PostLikeForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(post_like::table) + let post_like_form = (post_like_form, post_actions::liked.eq(now().nullable())); + insert_into(post_actions::table) .values(post_like_form) - .on_conflict((post_like::post_id, post_like::person_id)) + .on_conflict((post_actions::post_id, post_actions::person_id)) .do_update() .set(post_like_form) + .returning(Self::as_select()) .get_result::(conn) .await } @@ -291,10 +296,12 @@ impl Likeable for PostLike { pool: &mut DbPool<'_>, person_id: PersonId, post_id: PostId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(post_like::table.find((person_id, post_id))) - .execute(conn) + uplete::new(post_actions::table.find((person_id, post_id))) + .set_null(post_actions::like_score) + .set_null(post_actions::liked) + .get_result(conn) .await } } @@ -304,18 +311,24 @@ impl Saveable for PostSaved { type Form = PostSavedForm; async fn save(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(post_saved::table) + let post_saved_form = (post_saved_form, post_actions::saved.eq(now().nullable())); + insert_into(post_actions::table) .values(post_saved_form) - .on_conflict((post_saved::post_id, post_saved::person_id)) + .on_conflict((post_actions::post_id, post_actions::person_id)) .do_update() .set(post_saved_form) + .returning(Self::as_select()) .get_result::(conn) .await } - async fn unsave(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result { + async fn unsave( + pool: &mut DbPool<'_>, + post_saved_form: &PostSavedForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete(post_saved::table.find((post_saved_form.person_id, post_saved_form.post_id))) - .execute(conn) + uplete::new(post_actions::table.find((post_saved_form.person_id, post_saved_form.post_id))) + .set_null(post_actions::saved) + .get_result(conn) .await } } @@ -330,14 +343,22 @@ impl PostRead { let forms = post_ids .iter() - .map(|post_id| PostReadForm { - post_id: *post_id, - person_id, + .map(|post_id| { + ( + PostReadForm { + post_id: *post_id, + person_id, + }, + post_actions::read.eq(now().nullable()), + ) }) - .collect::>(); - insert_into(post_read::table) + .collect::>(); + + insert_into(post_actions::table) .values(forms) - .on_conflict_do_nothing() + .on_conflict((post_actions::person_id, post_actions::post_id)) + .do_update() + .set(post_actions::read.eq(now().nullable())) .execute(conn) .await .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead) @@ -347,15 +368,16 @@ impl PostRead { pool: &mut DbPool<'_>, post_ids: &[PostId], person_id_: PersonId, - ) -> LemmyResult { + ) -> LemmyResult { let conn = &mut get_conn(pool).await?; - diesel::delete( - post_read::table - .filter(post_read::post_id.eq_any(post_ids)) - .filter(post_read::person_id.eq(person_id_)), + uplete::new( + post_actions::table + .filter(post_actions::post_id.eq_any(post_ids.to_vec())) + .filter(post_actions::person_id.eq(person_id_)), ) - .execute(conn) + .set_null(post_actions::read) + .get_result(conn) .await .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead) } @@ -371,11 +393,18 @@ impl PostHide { let forms = post_ids .into_iter() - .map(|post_id| PostHideForm { post_id, person_id }) - .collect::>(); - insert_into(post_hide::table) + .map(|post_id| { + ( + PostHideForm { post_id, person_id }, + post_actions::hidden.eq(now().nullable()), + ) + }) + .collect::>(); + insert_into(post_actions::table) .values(forms) - .on_conflict_do_nothing() + .on_conflict((post_actions::person_id, post_actions::post_id)) + .do_update() + .set(post_actions::hidden.eq(now().nullable())) .execute(conn) .await } @@ -384,15 +413,16 @@ impl PostHide { pool: &mut DbPool<'_>, post_id_: HashSet, person_id_: PersonId, - ) -> Result { + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::delete( - post_hide::table - .filter(post_hide::post_id.eq_any(post_id_)) - .filter(post_hide::person_id.eq(person_id_)), + uplete::new( + post_actions::table + .filter(post_actions::post_id.eq_any(post_id_)) + .filter(post_actions::person_id.eq(person_id_)), ) - .execute(conn) + .set_null(post_actions::hidden) + .get_result(conn) .await } } @@ -417,7 +447,7 @@ mod tests { }, }, traits::{Crud, Likeable, Saveable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, uplete}, }; use chrono::DateTime; use lemmy_utils::error::LemmyResult; @@ -545,16 +575,16 @@ mod tests { assert_eq!(1, scheduled_post_count); let like_removed = PostLike::remove(pool, inserted_person.id, inserted_post.id).await?; - assert_eq!(1, like_removed); + assert_eq!(uplete::Count::only_updated(1), like_removed); let saved_removed = PostSaved::unsave(pool, &post_saved_form).await?; - assert_eq!(1, saved_removed); + assert_eq!(uplete::Count::only_updated(1), saved_removed); let read_removed = PostRead::mark_as_unread( pool, &[inserted_post.id, inserted_post2.id], inserted_person.id, ) .await?; - assert_eq!(2, read_removed); + assert_eq!(uplete::Count::only_deleted(2), read_removed); let num_deleted = Post::delete(pool, inserted_post.id).await? + Post::delete(pool, inserted_post2.id).await? diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 0397c939a..7ee60cc1e 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -30,11 +30,11 @@ pub mod sensitive; pub mod schema; #[cfg(feature = "full")] pub mod aliases { - use crate::schema::{community_moderator, person}; + use crate::schema::{community_actions, person}; diesel::alias!( + community_actions as creator_community_actions: CreatorCommunityActions, person as person1: Person1, person as person2: Person2, - community_moderator as community_moderator1: CommunityModerator1 ); } pub mod source; diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index cef12b6bd..9e80c4693 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -110,6 +110,16 @@ diesel::table! { } } +diesel::table! { + comment_actions (person_id, comment_id) { + person_id -> Int4, + comment_id -> Int4, + like_score -> Nullable, + liked -> Nullable, + saved -> Nullable, + } +} + diesel::table! { comment_aggregates (comment_id) { comment_id -> Int4, @@ -123,15 +133,6 @@ diesel::table! { } } -diesel::table! { - comment_like (person_id, comment_id) { - person_id -> Int4, - comment_id -> Int4, - score -> Int2, - published -> Timestamptz, - } -} - diesel::table! { comment_reply (id) { id -> Int4, @@ -156,14 +157,6 @@ diesel::table! { } } -diesel::table! { - comment_saved (person_id, comment_id) { - comment_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { use diesel::sql_types::*; use super::sql_types::CommunityVisibility; @@ -205,6 +198,23 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::CommunityFollowerState; + + community_actions (person_id, community_id) { + community_id -> Int4, + person_id -> Int4, + followed -> Nullable, + follow_state -> Nullable, + follow_approver_id -> Nullable, + blocked -> Nullable, + became_moderator -> Nullable, + received_ban -> Nullable, + ban_expires -> Nullable, + } +} + diesel::table! { community_aggregates (community_id) { community_id -> Int4, @@ -221,27 +231,6 @@ diesel::table! { } } -diesel::table! { - community_block (person_id, community_id) { - person_id -> Int4, - community_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - use diesel::sql_types::*; - use super::sql_types::CommunityFollowerState; - - community_follower (person_id, community_id) { - community_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - state -> CommunityFollowerState, - approver_id -> Nullable, - } -} - diesel::table! { community_language (community_id, language_id) { community_id -> Int4, @@ -249,23 +238,6 @@ diesel::table! { } } -diesel::table! { - community_moderator (person_id, community_id) { - community_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - community_person_ban (person_id, community_id) { - community_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - expires -> Nullable, - } -} - diesel::table! { custom_emoji (id) { id -> Int4, @@ -347,10 +319,10 @@ diesel::table! { } diesel::table! { - instance_block (person_id, instance_id) { + instance_actions (person_id, instance_id) { person_id -> Int4, instance_id -> Int4, - published -> Timestamptz, + blocked -> Nullable, } } @@ -703,6 +675,16 @@ diesel::table! { } } +diesel::table! { + person_actions (person_id, target_id) { + target_id -> Int4, + person_id -> Int4, + followed -> Nullable, + follow_pending -> Nullable, + blocked -> Nullable, + } +} + diesel::table! { person_aggregates (person_id) { person_id -> Int4, @@ -720,23 +702,6 @@ diesel::table! { } } -diesel::table! { - person_block (person_id, target_id) { - person_id -> Int4, - target_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - person_follower (follower_id, person_id) { - person_id -> Int4, - follower_id -> Int4, - published -> Timestamptz, - pending -> Bool, - } -} - diesel::table! { person_mention (id) { id -> Int4, @@ -747,15 +712,6 @@ diesel::table! { } } -diesel::table! { - person_post_aggregates (person_id, post_id) { - person_id -> Int4, - post_id -> Int4, - read_comments -> Int8, - published -> Timestamptz, - } -} - diesel::table! { post (id) { id -> Int4, @@ -788,6 +744,20 @@ diesel::table! { } } +diesel::table! { + post_actions (person_id, post_id) { + post_id -> Int4, + person_id -> Int4, + read -> Nullable, + read_comments -> Nullable, + read_comments_amount -> Nullable, + saved -> Nullable, + liked -> Nullable, + like_score -> Nullable, + hidden -> Nullable, + } +} + diesel::table! { post_aggregates (post_id) { post_id -> Int4, @@ -810,31 +780,6 @@ diesel::table! { } } -diesel::table! { - post_hide (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - -diesel::table! { - post_like (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - score -> Int2, - published -> Timestamptz, - } -} - -diesel::table! { - post_read (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { post_report (id) { id -> Int4, @@ -852,14 +797,6 @@ diesel::table! { } } -diesel::table! { - post_saved (person_id, post_id) { - post_id -> Int4, - person_id -> Int4, - published -> Timestamptz, - } -} - diesel::table! { private_message (id) { id -> Int4, @@ -1003,32 +940,24 @@ diesel::joinable!(admin_purge_post -> person (admin_person_id)); diesel::joinable!(comment -> language (language_id)); diesel::joinable!(comment -> person (creator_id)); diesel::joinable!(comment -> post (post_id)); +diesel::joinable!(comment_actions -> comment (comment_id)); +diesel::joinable!(comment_actions -> person (person_id)); diesel::joinable!(comment_aggregates -> comment (comment_id)); -diesel::joinable!(comment_like -> comment (comment_id)); -diesel::joinable!(comment_like -> person (person_id)); diesel::joinable!(comment_reply -> comment (comment_id)); diesel::joinable!(comment_reply -> person (recipient_id)); diesel::joinable!(comment_report -> comment (comment_id)); -diesel::joinable!(comment_saved -> comment (comment_id)); -diesel::joinable!(comment_saved -> person (person_id)); diesel::joinable!(community -> instance (instance_id)); +diesel::joinable!(community_actions -> community (community_id)); diesel::joinable!(community_aggregates -> community (community_id)); -diesel::joinable!(community_block -> community (community_id)); -diesel::joinable!(community_block -> person (person_id)); -diesel::joinable!(community_follower -> community (community_id)); diesel::joinable!(community_language -> community (community_id)); diesel::joinable!(community_language -> language (language_id)); -diesel::joinable!(community_moderator -> community (community_id)); -diesel::joinable!(community_moderator -> person (person_id)); -diesel::joinable!(community_person_ban -> community (community_id)); -diesel::joinable!(community_person_ban -> person (person_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); diesel::joinable!(federation_blocklist -> instance (instance_id)); diesel::joinable!(federation_queue_state -> instance (instance_id)); -diesel::joinable!(instance_block -> instance (instance_id)); -diesel::joinable!(instance_block -> person (person_id)); +diesel::joinable!(instance_actions -> instance (instance_id)); +diesel::joinable!(instance_actions -> person (person_id)); diesel::joinable!(local_image -> local_user (local_user_id)); diesel::joinable!(local_site -> site (site_id)); diesel::joinable!(local_site_rate_limit -> local_site (local_site_id)); @@ -1060,24 +989,16 @@ diesel::joinable!(person_aggregates -> person (person_id)); diesel::joinable!(person_ban -> person (person_id)); diesel::joinable!(person_mention -> comment (comment_id)); diesel::joinable!(person_mention -> person (recipient_id)); -diesel::joinable!(person_post_aggregates -> person (person_id)); -diesel::joinable!(person_post_aggregates -> post (post_id)); diesel::joinable!(post -> community (community_id)); diesel::joinable!(post -> language (language_id)); diesel::joinable!(post -> person (creator_id)); +diesel::joinable!(post_actions -> person (person_id)); +diesel::joinable!(post_actions -> post (post_id)); diesel::joinable!(post_aggregates -> community (community_id)); diesel::joinable!(post_aggregates -> instance (instance_id)); diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); -diesel::joinable!(post_hide -> person (person_id)); -diesel::joinable!(post_hide -> post (post_id)); -diesel::joinable!(post_like -> person (person_id)); -diesel::joinable!(post_like -> post (post_id)); -diesel::joinable!(post_read -> person (person_id)); -diesel::joinable!(post_read -> post (post_id)); diesel::joinable!(post_report -> post (post_id)); -diesel::joinable!(post_saved -> person (person_id)); -diesel::joinable!(post_saved -> post (post_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); @@ -1093,18 +1014,14 @@ diesel::allow_tables_to_appear_in_same_query!( admin_purge_post, captcha_answer, comment, + comment_actions, comment_aggregates, - comment_like, comment_reply, comment_report, - comment_saved, community, + community_actions, community_aggregates, - community_block, - community_follower, community_language, - community_moderator, - community_person_ban, custom_emoji, custom_emoji_keyword, email_verification, @@ -1113,7 +1030,7 @@ diesel::allow_tables_to_appear_in_same_query!( federation_queue_state, image_details, instance, - instance_block, + instance_actions, language, local_image, local_site, @@ -1138,19 +1055,14 @@ diesel::allow_tables_to_appear_in_same_query!( oauth_provider, password_reset_request, person, + person_actions, person_aggregates, person_ban, - person_block, - person_follower, person_mention, - person_post_aggregates, post, + post_actions, post_aggregates, - post_hide, - post_like, - post_read, post_report, - post_saved, private_message, private_message_report, received_activity, diff --git a/crates/db_schema/src/source/comment.rs b/crates/db_schema/src/source/comment.rs index 7e65638ed..be9aa7873 100644 --- a/crates/db_schema/src/source/comment.rs +++ b/crates/db_schema/src/source/comment.rs @@ -2,9 +2,11 @@ use crate::newtypes::LtreeDef; use crate::newtypes::{CommentId, DbUrl, LanguageId, PersonId, PostId}; #[cfg(feature = "full")] -use crate::schema::{comment, comment_like, comment_saved}; +use crate::schema::{comment, comment_actions}; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; +#[cfg(feature = "full")] use diesel_ltree::Ltree; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -97,22 +99,27 @@ pub struct CommentUpdateForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))] -#[cfg_attr(feature = "full", diesel(table_name = comment_like))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, comment_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommentLike { pub person_id: PersonId, pub comment_id: CommentId, + #[cfg_attr(feature = "full", diesel(select_expression = comment_actions::like_score.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub score: i16, + #[cfg_attr(feature = "full", diesel(select_expression = comment_actions::liked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = comment_like))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] pub struct CommentLikeForm { pub person_id: PersonId, pub comment_id: CommentId, + #[cfg_attr(feature = "full", diesel(column_name = like_score))] pub score: i16, } @@ -122,17 +129,19 @@ pub struct CommentLikeForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))] -#[cfg_attr(feature = "full", diesel(table_name = comment_saved))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, comment_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommentSaved { pub comment_id: CommentId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = comment_actions::saved.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = comment_saved))] +#[cfg_attr(feature = "full", diesel(table_name = comment_actions))] pub struct CommentSavedForm { pub comment_id: CommentId, pub person_id: PersonId, diff --git a/crates/db_schema/src/source/community.rs b/crates/db_schema/src/source/community.rs index 95d2a67c3..f65ef06f9 100644 --- a/crates/db_schema/src/source/community.rs +++ b/crates/db_schema/src/source/community.rs @@ -1,5 +1,5 @@ #[cfg(feature = "full")] -use crate::schema::{community, community_follower, community_moderator, community_person_ban}; +use crate::schema::{community, community_actions}; use crate::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, sensitive::SensitiveString, @@ -7,6 +7,8 @@ use crate::{ CommunityVisibility, }; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use strum::{Display, EnumString}; @@ -163,18 +165,20 @@ pub struct CommunityUpdateForm { feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_moderator))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityModerator { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::became_moderator.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_moderator))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityModeratorForm { pub community_id: CommunityId, pub person_id: PersonId, @@ -189,22 +193,26 @@ pub struct CommunityModeratorForm { feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_person_ban))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityPersonBan { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::received_ban.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, + #[cfg_attr(feature = "full", diesel(column_name = ban_expires))] pub expires: Option>, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_person_ban))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityPersonBanForm { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = ban_expires))] pub expires: Option>>, } @@ -231,25 +239,32 @@ pub enum CommunityFollowerState { feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_follower))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityFollower { pub community_id: CommunityId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::followed.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::follow_state.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub state: CommunityFollowerState, + #[cfg_attr(feature = "full", diesel(column_name = follow_approver_id))] pub approver_id: Option, } #[derive(Clone, derive_new::new)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_follower))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityFollowerForm { pub community_id: CommunityId, pub person_id: PersonId, #[new(default)] + #[cfg_attr(feature = "full", diesel(column_name = follow_state))] pub state: Option, #[new(default)] + #[cfg_attr(feature = "full", diesel(column_name = follow_approver_id))] pub approver_id: Option, } diff --git a/crates/db_schema/src/source/community_block.rs b/crates/db_schema/src/source/community_block.rs index 7d43af173..a7c23419c 100644 --- a/crates/db_schema/src/source/community_block.rs +++ b/crates/db_schema/src/source/community_block.rs @@ -1,7 +1,9 @@ use crate::newtypes::{CommunityId, PersonId}; #[cfg(feature = "full")] -use crate::schema::community_block; +use crate::schema::community_actions; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -13,17 +15,19 @@ use serde::{Deserialize, Serialize}; feature = "full", diesel(belongs_to(crate::source::community::Community)) )] -#[cfg_attr(feature = "full", diesel(table_name = community_block))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, community_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct CommunityBlock { pub person_id: PersonId, pub community_id: CommunityId, + #[cfg_attr(feature = "full", diesel(select_expression = community_actions::blocked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_block))] +#[cfg_attr(feature = "full", diesel(table_name = community_actions))] pub struct CommunityBlockForm { pub person_id: PersonId, pub community_id: CommunityId, diff --git a/crates/db_schema/src/source/instance_block.rs b/crates/db_schema/src/source/instance_block.rs index 4eebbf1a8..e1963c894 100644 --- a/crates/db_schema/src/source/instance_block.rs +++ b/crates/db_schema/src/source/instance_block.rs @@ -1,7 +1,9 @@ use crate::newtypes::{InstanceId, PersonId}; #[cfg(feature = "full")] -use crate::schema::instance_block; +use crate::schema::instance_actions; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -13,17 +15,19 @@ use serde::{Deserialize, Serialize}; feature = "full", diesel(belongs_to(crate::source::instance::Instance)) )] -#[cfg_attr(feature = "full", diesel(table_name = instance_block))] +#[cfg_attr(feature = "full", diesel(table_name = instance_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, instance_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct InstanceBlock { pub person_id: PersonId, pub instance_id: InstanceId, + #[cfg_attr(feature = "full", diesel(select_expression = instance_actions::blocked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = instance_block))] +#[cfg_attr(feature = "full", diesel(table_name = instance_actions))] pub struct InstanceBlockForm { pub person_id: PersonId, pub instance_id: InstanceId, diff --git a/crates/db_schema/src/source/person.rs b/crates/db_schema/src/source/person.rs index d8b0a5b1a..9c2a2d426 100644 --- a/crates/db_schema/src/source/person.rs +++ b/crates/db_schema/src/source/person.rs @@ -1,11 +1,13 @@ #[cfg(feature = "full")] -use crate::schema::{person, person_follower}; +use crate::schema::{person, person_actions}; use crate::{ newtypes::{DbUrl, InstanceId, PersonId}, sensitive::SensitiveString, source::placeholder_apub_url, }; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -133,21 +135,30 @@ pub struct PersonUpdateForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] -#[cfg_attr(feature = "full", diesel(table_name = person_follower))] -#[cfg_attr(feature = "full", diesel(primary_key(follower_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, target_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PersonFollower { + #[cfg_attr(feature = "full", diesel(column_name = target_id))] pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = person_id))] pub follower_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = person_actions::followed.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, + #[cfg_attr(feature = "full", diesel(select_expression = person_actions::follow_pending.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub pending: bool, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_follower))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] pub struct PersonFollowerForm { + #[cfg_attr(feature = "full", diesel(column_name = target_id))] pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = person_id))] pub follower_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = follow_pending))] pub pending: bool, } diff --git a/crates/db_schema/src/source/person_block.rs b/crates/db_schema/src/source/person_block.rs index 43048fb39..ec988a60f 100644 --- a/crates/db_schema/src/source/person_block.rs +++ b/crates/db_schema/src/source/person_block.rs @@ -1,7 +1,9 @@ use crate::newtypes::PersonId; #[cfg(feature = "full")] -use crate::schema::person_block; +use crate::schema::person_actions; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -10,17 +12,19 @@ use serde::{Deserialize, Serialize}; derive(Queryable, Selectable, Associations, Identifiable) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::person::Person)))] -#[cfg_attr(feature = "full", diesel(table_name = person_block))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, target_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PersonBlock { pub person_id: PersonId, pub target_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = person_actions::blocked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_block))] +#[cfg_attr(feature = "full", diesel(table_name = person_actions))] pub struct PersonBlockForm { pub person_id: PersonId, pub target_id: PersonId, diff --git a/crates/db_schema/src/source/post.rs b/crates/db_schema/src/source/post.rs index 3417f87b5..bed659a10 100644 --- a/crates/db_schema/src/source/post.rs +++ b/crates/db_schema/src/source/post.rs @@ -1,7 +1,9 @@ use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId}; #[cfg(feature = "full")] -use crate::schema::{post, post_hide, post_like, post_read, post_saved}; +use crate::schema::{post, post_actions}; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use diesel::{dsl, expression_methods::NullableExpressionMethods}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -149,22 +151,27 @@ pub struct PostUpdateForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_like))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] #[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostLike { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::like_score.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub score: i16, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::liked.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[derive(Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_like))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub struct PostLikeForm { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(column_name = like_score))] pub score: i16, } @@ -174,17 +181,19 @@ pub struct PostLikeForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_saved))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostSaved { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::saved.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_saved))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub struct PostSavedForm { pub post_id: PostId, pub person_id: PersonId, @@ -196,17 +205,19 @@ pub struct PostSavedForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_read))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostRead { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::read.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_read))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub(crate) struct PostReadForm { pub post_id: PostId, pub person_id: PersonId, @@ -218,17 +229,19 @@ pub(crate) struct PostReadForm { derive(Identifiable, Queryable, Selectable, Associations) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] -#[cfg_attr(feature = "full", diesel(table_name = post_hide))] -#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] +#[cfg_attr(feature = "full", diesel(primary_key(person_id, post_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] pub struct PostHide { pub post_id: PostId, pub person_id: PersonId, + #[cfg_attr(feature = "full", diesel(select_expression = post_actions::hidden.assume_not_null()))] + #[cfg_attr(feature = "full", diesel(select_expression_type = dsl::AssumeNotNull))] pub published: DateTime, } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = post_hide))] +#[cfg_attr(feature = "full", diesel(table_name = post_actions))] pub(crate) struct PostHideForm { pub post_id: PostId, pub person_id: PersonId, diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index 74f5ea009..bc30c6fb9 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -1,6 +1,6 @@ use crate::{ newtypes::{CommunityId, DbUrl, PersonId}, - utils::{get_conn, DbPool}, + utils::{get_conn, uplete, DbPool}, }; use diesel::{ associations::HasTable, @@ -76,7 +76,7 @@ pub trait Followable { ) -> Result where Self: Sized; - async fn unfollow(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unfollow(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -87,7 +87,7 @@ pub trait Joinable { async fn join(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn leave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn leave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -103,7 +103,7 @@ pub trait Likeable { pool: &mut DbPool<'_>, person_id: PersonId, item_id: Self::IdType, - ) -> Result + ) -> Result where Self: Sized; } @@ -114,7 +114,7 @@ pub trait Bannable { async fn ban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn unban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unban(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -125,7 +125,7 @@ pub trait Saveable { async fn save(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn unsave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unsave(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } @@ -136,7 +136,7 @@ pub trait Blockable { async fn block(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; - async fn unblock(pool: &mut DbPool<'_>, form: &Self::Form) -> Result + async fn unblock(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; } diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 03f1bb8ca..bb7edb13f 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -1,18 +1,30 @@ +pub mod uplete; + use crate::{newtypes::DbUrl, CommentSortType, PostSortType}; use chrono::{DateTime, TimeDelta, Utc}; use deadpool::Runtime; use diesel::{ + dsl, + expression::AsExpression, helper_types::AsExprOf, pg::Pg, query_builder::{Query, QueryFragment}, - query_dsl::methods::LimitDsl, + query_dsl::methods::{FilterDsl, FindDsl, LimitDsl}, + query_source::{Alias, AliasSource, AliasedField}, result::{ ConnectionError, ConnectionResult, Error::{self as DieselError, QueryBuilderError}, }, - sql_types::{self, Timestamptz}, + sql_types::{self, SingleValue, Timestamptz}, + Column, + Expression, + ExpressionMethods, IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QuerySource, + Table, }; use diesel_async::{ pg::AsyncPgConnection, @@ -23,6 +35,7 @@ use diesel_async::{ }, AsyncConnection, }; +use diesel_bind_if_some::BindIfSome; use futures_util::{future::BoxFuture, Future, FutureExt}; use i_love_jesus::CursorKey; use lemmy_utils::{ @@ -540,6 +553,117 @@ pub fn now() -> AsExprOf { diesel::dsl::now.into_sql::() } +/// Trait alias for a type that can be converted to an SQL tuple using `IntoSql::into_sql` +pub trait AsRecord: Expression + AsExpression> +where + Self::SqlType: 'static, +{ +} + +impl>> AsRecord for T where + T::SqlType: 'static +{ +} + +/// Output of `IntoSql::into_sql` for a type that implements `AsRecord` +pub type AsRecordOutput = dsl::AsExprOf::SqlType>>; + +/// Output of `t.on((l0, l1).into_sql().eq((r0, r1)))` +type OnTupleEq = dsl::On, (R0, R1)>>; + +/// Creates an `ON` clause for a table where a person ID and another column are used as the +/// primary key. Use with the `QueryDsl::left_join` method. +/// +/// This example modifies a query to make columns in `community_actions` available: +/// +/// ``` +/// community::table +/// .left_join(actions( +/// community_actions::table, +/// my_person_id, +/// community::id, +/// )) +/// ``` +pub fn actions( + actions_table: T, + person_id: Option

, + target_id: C, +) -> OnTupleEq, K1, BindIfSome>, C> +where + T: Table + Copy, + K0: Expression, + P: AsExpression, + (dsl::Nullable, K1): AsRecord, + (BindIfSome>, C): + AsExpression<, K1)> as Expression>::SqlType>, +{ + let (k0, k1) = actions_table.primary_key(); + actions_table.on((k0.nullable(), k1).into_sql().eq(( + BindIfSome(person_id.map(diesel::IntoSql::into_sql)), + target_id, + ))) +} + +/// Like `actions` but `actions_table` is an alias and person id is not nullable +#[allow(clippy::type_complexity)] +pub fn actions_alias( + actions_table: Alias, + person_id: P, + target_id: C, +) -> OnTupleEq, AliasedField, AliasedField, P, C> +where + Alias: QuerySource + Copy, + T: AliasSource> + Default, + K0: Column, + K1: Column
, + (AliasedField, AliasedField): AsRecord, + (P, C): AsExpression< + , AliasedField)> as Expression>::SqlType, + >, +{ + let (k0, k1) = T::default().target().primary_key(); + actions_table.on( + (actions_table.field(k0), actions_table.field(k1)) + .into_sql() + .eq((person_id, target_id)), + ) +} + +/// `action_query(table_name::action_name)` is the same as +/// `table_name::table.filter(table_name::action_name.is_not_null())`. +pub fn action_query(column: C) -> dsl::Filter> +where + C: Column>, SqlType: SingleValue>, +{ + action_query_with_fn(column, |t| t) +} + +/// `find_action(table_name::action_name, key)` is the same as +/// `table_name::table.find(key).filter(table_name::action_name.is_not_null())`. +pub fn find_action( + column: C, + key: K, +) -> dsl::Filter, dsl::IsNotNull> +where + C: + Column>>, SqlType: SingleValue>, +{ + action_query_with_fn(column, |t| t.find(key)) +} + +/// `action_query_with_fn(table_name::action_name, f)` is the same as +/// `f(table_name::table).filter(table_name::action_name.is_not_null())`. +fn action_query_with_fn( + column: C, + f: impl FnOnce(C::Table) -> Q, +) -> dsl::Filter> +where + C: Column, + Q: FilterDsl>, +{ + f(C::Table::default()).filter(column.is_not_null()) +} + pub type ResultFuture<'a, T> = BoxFuture<'a, Result>; pub trait ReadFn<'a, T, Args>: Fn(DbConn<'a>, Args) -> ResultFuture<'a, T> {} diff --git a/crates/db_schema/src/utils/uplete.rs b/crates/db_schema/src/utils/uplete.rs new file mode 100644 index 000000000..8c5262b90 --- /dev/null +++ b/crates/db_schema/src/utils/uplete.rs @@ -0,0 +1,423 @@ +use diesel::{ + associations::HasTable, + dsl, + expression::{is_aggregate, ValidGrouping}, + pg::Pg, + query_builder::{AsQuery, AstPass, Query, QueryFragment, QueryId}, + query_dsl::methods::{FilterDsl, SelectDsl}, + result::Error, + sql_types, + Column, + Expression, + Table, +}; +use std::any::TypeId; +use tuplex::IntoArray; + +/// Set columns (each specified with `UpleteBuilder::set_null`) to null in the rows found by +/// `query`, and delete rows that have no remaining non-null values outside of the primary key +pub fn new(query: Q) -> UpleteBuilder::PrimaryKey>> +where + Q: AsQuery + HasTable, + Q::Table: Default, + Q::Query: SelectDsl<::PrimaryKey>, + + // For better error messages + UpleteBuilder: AsQuery, +{ + UpleteBuilder { + query: query.as_query().select(Q::Table::default().primary_key()), + set_null_columns: Vec::new(), + } +} + +pub struct UpleteBuilder { + query: Q, + set_null_columns: Vec, +} + +impl UpleteBuilder { + pub fn set_null + Into>(mut self, column: C) -> Self { + self.set_null_columns.push(column.into()); + self + } +} + +impl AsQuery for UpleteBuilder +where + Q: HasTable, + Q::Table: Default + QueryFragment + Send + 'static, + ::PrimaryKey: IntoArray + QueryFragment + Send + 'static, + ::AllColumns: IntoArray, + <::PrimaryKey as IntoArray>::Output: IntoIterator, + <::AllColumns as IntoArray>::Output: IntoIterator, + Q: Clone + FilterDsl + FilterDsl>, + dsl::Filter: QueryFragment + Send + 'static, + dsl::Filter>: QueryFragment + Send + 'static, +{ + type Query = UpleteQuery; + + type SqlType = (sql_types::BigInt, sql_types::BigInt); + + fn as_query(self) -> Self::Query { + let table = Q::Table::default; + let deletion_condition = AllNull( + Q::Table::all_columns() + .into_array() + .into_iter() + .filter(|c: &DynColumn| { + table() + .primary_key() + .into_array() + .into_iter() + .chain(self.set_null_columns.iter().cloned()) + .all(|excluded_column| excluded_column.type_id != c.type_id) + }) + .collect::>(), + ); + UpleteQuery { + // Updated rows and deleted rows must not overlap, so updating all rows and using the returned + // new rows to determine which ones to delete is not an option. + // + // https://www.postgresql.org/docs/16/queries-with.html#QUERIES-WITH-MODIFYING + // + // "Trying to update the same row twice in a single statement is not supported. Only one of + // the modifications takes place, but it is not easy (and sometimes not possible) to reliably + // predict which one. This also applies to deleting a row that was already updated in the same + // statement: only the update is performed." + update_subquery: Box::new( + self + .query + .clone() + .filter(dsl::not(deletion_condition.clone())), + ), + delete_subquery: Box::new(self.query.filter(deletion_condition)), + table: Box::new(table()), + primary_key: Box::new(table().primary_key()), + set_null_columns: self.set_null_columns, + } + } +} + +pub struct UpleteQuery { + update_subquery: Box + Send + 'static>, + delete_subquery: Box + Send + 'static>, + table: Box + Send + 'static>, + primary_key: Box + Send + 'static>, + set_null_columns: Vec, +} + +impl QueryId for UpleteQuery { + type QueryId = (); + + const HAS_STATIC_QUERY_ID: bool = false; +} + +impl Query for UpleteQuery { + type SqlType = (sql_types::BigInt, sql_types::BigInt); +} + +impl QueryFragment for UpleteQuery { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> Result<(), Error> { + assert_ne!(self.set_null_columns.len(), 0, "`set_null` was not called"); + + // Declare `update_keys` and `delete_keys` CTEs, which select primary keys + for (prefix, subquery) in [ + ("WITH update_keys", &self.update_subquery), + (", delete_keys", &self.delete_subquery), + ] { + out.push_sql(prefix); + out.push_sql(" AS ("); + subquery.walk_ast(out.reborrow())?; + out.push_sql(" FOR UPDATE)"); + } + + // Update rows that are referenced in `update_keys` + out.push_sql(", update_result AS (UPDATE "); + self.table.walk_ast(out.reborrow())?; + let mut item_prefix = " SET "; + for column in &self.set_null_columns { + out.push_sql(item_prefix); + out.push_identifier(column.name)?; + out.push_sql(" = NULL"); + item_prefix = ","; + } + out.push_sql(" WHERE ("); + self.primary_key.walk_ast(out.reborrow())?; + out.push_sql(") = ANY (SELECT * FROM update_keys) RETURNING 1)"); + + // Delete rows that are referenced in `delete_keys` + out.push_sql(", delete_result AS (DELETE FROM "); + self.table.walk_ast(out.reborrow())?; + out.push_sql(" WHERE ("); + self.primary_key.walk_ast(out.reborrow())?; + out.push_sql(") = ANY (SELECT * FROM delete_keys) RETURNING 1)"); + + // Count updated rows and deleted rows (`RETURNING 1` makes this possible) + out.push_sql(" SELECT (SELECT count(*) FROM update_result)"); + out.push_sql(", (SELECT count(*) FROM delete_result)"); + + Ok(()) + } +} + +// Types other than `DynColumn` are only used in tests +#[derive(Clone)] +pub struct AllNull(Vec); + +impl Expression for AllNull { + type SqlType = sql_types::Bool; +} + +impl ValidGrouping<()> for AllNull { + type IsAggregate = is_aggregate::No; +} + +impl> QueryFragment for AllNull { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> Result<(), Error> { + // Must produce a valid expression even if `self.0` is empty + out.push_sql("(TRUE"); + for item in &self.0 { + out.push_sql(" AND ("); + item.walk_ast(out.reborrow())?; + out.push_sql(" IS NULL)"); + } + out.push_sql(")"); + + Ok(()) + } +} + +#[derive(Clone)] +pub struct DynColumn { + type_id: TypeId, + name: &'static str, +} + +impl From for DynColumn { + fn from(_value: T) -> Self { + DynColumn { + type_id: TypeId::of::(), + name: T::NAME, + } + } +} + +impl QueryFragment for DynColumn { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> Result<(), Error> { + out.push_identifier(self.name) + } +} + +#[derive(Queryable, PartialEq, Eq, Debug)] +pub struct Count { + pub updated: i64, + pub deleted: i64, +} + +impl Count { + pub fn only_updated(n: i64) -> Self { + Count { + updated: n, + deleted: 0, + } + } + + pub fn only_deleted(n: i64) -> Self { + Count { + updated: 0, + deleted: n, + } + } +} + +#[cfg(test)] +mod tests { + use super::AllNull; + use crate::utils::{build_db_pool_for_tests, get_conn, DbConn}; + use diesel::{ + debug_query, + insert_into, + pg::Pg, + query_builder::{AsQuery, QueryId}, + select, + sql_types, + AppearsOnTable, + ExpressionMethods, + IntoSql, + QueryDsl, + SelectableExpression, + }; + use diesel_async::{RunQueryDsl, SimpleAsyncConnection}; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + impl AppearsOnTable for AllNull {} + + impl SelectableExpression for AllNull {} + + impl QueryId for AllNull { + type QueryId = (); + const HAS_STATIC_QUERY_ID: bool = false; + } + + diesel::table! { + t (id1, id2) { + // uplete doesn't work for non-tuple primary key + id1 -> Int4, + id2 -> Int4, + a -> Nullable, + b -> Nullable, + } + } + + async fn expect_rows( + conn: &mut DbConn<'_>, + expected: &[(Option, Option)], + ) -> LemmyResult<()> { + let rows: Vec<(Option, Option)> = t::table + .select((t::a, t::b)) + .order_by(t::id1) + .load(conn) + .await?; + assert_eq!(expected, &rows); + + Ok(()) + } + + // Main purpose of this test is to check accuracy of the returned `Count`, which other modules' + // tests rely on + #[tokio::test] + #[serial] + async fn test_count() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut conn = get_conn(pool).await?; + + conn + .batch_execute("CREATE TABLE t (id1 serial, id2 int NOT NULL DEFAULT 1, a int, b int, PRIMARY KEY (id1, id2));") + .await?; + expect_rows(&mut conn, &[]).await?; + + insert_into(t::table) + .values(&[ + (t::a.eq(Some(1)), t::b.eq(Some(2))), + (t::a.eq(Some(3)), t::b.eq(None)), + (t::a.eq(Some(4)), t::b.eq(Some(5))), + ]) + .execute(&mut conn) + .await?; + expect_rows( + &mut conn, + &[(Some(1), Some(2)), (Some(3), None), (Some(4), Some(5))], + ) + .await?; + + let count1 = super::new(t::table) + .set_null(t::a) + .get_result(&mut conn) + .await?; + assert_eq!( + super::Count { + updated: 2, + deleted: 1 + }, + count1 + ); + expect_rows(&mut conn, &[(None, Some(2)), (None, Some(5))]).await?; + + let count2 = super::new(t::table) + .set_null(t::b) + .get_result(&mut conn) + .await?; + assert_eq!(super::Count::only_deleted(2), count2); + expect_rows(&mut conn, &[]).await?; + + conn.batch_execute("DROP TABLE t;").await?; + + Ok(()) + } + + fn expected_sql(check_null: &str, set_null: &str) -> String { + let with_queries = { + let key = r#""t"."id1", "t"."id2""#; + let t = r#""t""#; + + let update_keys = format!("SELECT {key} FROM {t} WHERE NOT (({check_null})) FOR UPDATE"); + let delete_keys = format!("SELECT {key} FROM {t} WHERE ({check_null}) FOR UPDATE"); + let update_result = format!( + "UPDATE {t} SET {set_null} WHERE ({key}) = ANY (SELECT * FROM update_keys) RETURNING 1" + ); + let delete_result = + format!("DELETE FROM {t} WHERE ({key}) = ANY (SELECT * FROM delete_keys) RETURNING 1"); + + format!("update_keys AS ({update_keys}), delete_keys AS ({delete_keys}), update_result AS ({update_result}), delete_result AS ({delete_result})") + }; + let update_count = "SELECT count(*) FROM update_result"; + let delete_count = "SELECT count(*) FROM delete_result"; + + format!(r#"WITH {with_queries} SELECT ({update_count}), ({delete_count}) -- binds: []"#) + } + + #[test] + fn test_generated_sql() { + // Unlike the `get_result` method, `debug_query` does not automatically call `as_query` + assert_eq!( + debug_query::(&super::new(t::table).set_null(t::b).as_query()).to_string(), + expected_sql(r#"TRUE AND ("a" IS NULL)"#, r#""b" = NULL"#) + ); + assert_eq!( + debug_query::( + &super::new(t::table) + .set_null(t::a) + .set_null(t::b) + .as_query() + ) + .to_string(), + expected_sql(r#"TRUE"#, r#""a" = NULL,"b" = NULL"#) + ); + } + + #[test] + fn test_count_methods() { + assert_eq!( + super::Count::only_updated(1), + super::Count { + updated: 1, + deleted: 0 + } + ); + assert_eq!( + super::Count::only_deleted(1), + super::Count { + updated: 0, + deleted: 1 + } + ); + } + + #[tokio::test] + #[serial] + async fn test_all_null() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut conn = get_conn(pool).await?; + + let some = Some(1).into_sql::>(); + let none = None::.into_sql::>(); + + // Allows type inference for `vec![]` + let mut all_null = |items| select(AllNull(items)).get_result::(&mut conn); + + assert!(all_null(vec![]).await?); + assert!(all_null(vec![none]).await?); + assert!(all_null(vec![none, none]).await?); + assert!(all_null(vec![none, none, none]).await?); + assert!(!all_null(vec![some]).await?); + assert!(!all_null(vec![some, none]).await?); + assert!(!all_null(vec![none, some, none]).await?); + + Ok(()) + } +} diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 06c05639d..278dc5c22 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -11,25 +11,33 @@ use diesel::{ }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{CommentId, CommentReportId, CommunityId, PersonId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, comment_report, - comment_saved, community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, + person_actions, post, }, source::community::CommunityFollower, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + utils::{ + actions, + actions_alias, + functions::coalesce, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, }; fn queries<'a>() -> Queries< @@ -46,40 +54,20 @@ fn queries<'a>() -> Queries< .inner_join( comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)), ) - .left_join( - comment_like::table.on( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(my_person_id)), - ), - ) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment_report::comment_id, + )) .left_join( aliases::person2 .on(comment_report::resolver_id.eq(aliases::person2.field(person::id).nullable())), ) - .left_join( - community_person_ban::table.on( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)) - .and( - community_person_ban::expires - .is_null() - .or(community_person_ban::expires.gt(now)), - ), - ), - ) - .left_join( - aliases::community_moderator1.on( - community::id - .eq(aliases::community_moderator1.field(community_moderator::community_id)) - .and( - aliases::community_moderator1 - .field(community_moderator::person_id) - .eq(comment::creator_id), - ), - ), - ) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .left_join( local_user::table.on( comment::creator_id @@ -87,27 +75,16 @@ fn queries<'a>() -> Queries< .and(local_user::admin.eq(true)), ), ) - .left_join( - person_block::table.on( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), - ), - ) - .left_join( - community_follower::table.on( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(my_person_id)), - ), - ) - .left_join( - comment_saved::table.on( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(my_person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(my_person_id), + comment::creator_id, + )) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) .select(( comment_report::all_columns, comment::all_columns, @@ -116,16 +93,28 @@ fn queries<'a>() -> Queries< person::all_columns, aliases::person1.fields(person::all_columns), comment_aggregates::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - aliases::community_moderator1 - .field(community_moderator::community_id) + coalesce( + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null() + .or( + creator_community_actions + .field(community_actions::ban_expires) + .nullable() + .gt(now), + ), + false, + ), + creator_community_actions + .field(community_actions::became_moderator) .nullable() .is_not_null(), local_user::admin.nullable().is_not_null(), - person_block::target_id.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), CommunityFollower::select_subscribed_type(), - comment_saved::published.nullable().is_not_null(), - comment_like::score.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), aliases::person2.fields(person::all_columns).nullable(), )) }; @@ -167,19 +156,10 @@ fn queries<'a>() -> Queries< // If its not an admin, get only the ones you mod if !user.local_user.admin { - query - .inner_join( - community_moderator::table.on( - community_moderator::community_id - .eq(post::community_id) - .and(community_moderator::person_id.eq(user.person.id)), - ), - ) - .load::(&mut conn) - .await - } else { - query.load::(&mut conn).await + query = query.filter(community_actions::became_moderator.is_not_null()); } + + query.load::(&mut conn).await }; Queries::new(read, list) @@ -222,10 +202,11 @@ impl CommentReportView { if !admin { query .inner_join( - community_moderator::table.on( - community_moderator::community_id + community_actions::table.on( + community_actions::community_id .eq(post::community_id) - .and(community_moderator::person_id.eq(my_person_id)), + .and(community_actions::person_id.eq(my_person_id)) + .and(community_actions::became_moderator.is_not_null()), ), ) .select(count(comment_report::id)) diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 0521e401c..22b7b3de4 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -3,11 +3,8 @@ use diesel::{ dsl::{exists, not}, pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, PgTextExpressionMethods, @@ -16,23 +13,20 @@ use diesel::{ use diesel_async::RunQueryDsl; use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions}; use lemmy_db_schema::{ + aliases::creator_community_actions, impls::local_user::LocalUserOptionHelper, newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, - comment_saved, community, - community_block, - community_follower, - community_moderator, - community_person_ban, - instance_block, + community_actions, + instance_actions, local_user, local_user_language, person, - person_block, + person_actions, post, }, source::{ @@ -40,7 +34,17 @@ use lemmy_db_schema::{ local_user::LocalUser, site::Site, }, - utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + utils::{ + actions, + actions_alias, + fuzzy_search, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, CommentSortType, CommunityVisibility, ListingType, @@ -50,64 +54,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>, impl ListFn<'a, CommentView, (CommentQuery<'a>, &'a Site)>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let is_community_followed = |person_id| { - community_follower::table - .filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(CommunityFollower::select_subscribed_type()) - .single_value() - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - comment_like::table - .filter( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(person_id)), - ) - .select(comment_like::score.nullable()) - .single_value() - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - community::id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(comment::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( comment::creator_id @@ -117,67 +63,56 @@ fn queries<'a>() -> Queries< ); let all_joins = move |query: comment::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression< - _, - Pg, - SqlType = sql_types::Nullable, - >, - > = if let Some(person_id) = my_person_id { - Box::new(is_community_followed(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - query .inner_join(person::table) .inner_join(post::table) .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(comment_aggregates::table) - .left_join( - comment_saved::table.on( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(my_person_id.unwrap_or(PersonId(-1)))), - ), - ) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions( + comment_actions::table, + my_person_id, + comment_aggregates::comment_id, + )) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions( + instance_actions::table, + my_person_id, + community::instance_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .select(( comment::all_columns, person::all_columns, post::all_columns, community::all_columns, comment_aggregates::all_columns, - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, - subscribed_type_selection, - comment_saved::person_id.nullable().is_not_null(), - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), )) }; @@ -197,15 +132,7 @@ fn queries<'a>() -> Queries< query = query.filter( community::visibility .ne(CommunityVisibility::Private) - .or(exists( - community_follower::table.filter( - post::community_id.eq(community_follower::community_id).and( - community_follower::person_id - .eq(my_local_user.map(|l| l.person_id).unwrap_or_default()) - .and(community_follower::state.eq(CommunityFollowerState::Accepted)), - ), - ), - )), + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } query.first(&mut conn).await @@ -213,7 +140,6 @@ fn queries<'a>() -> Queries< let list = move |mut conn: DbConn<'a>, (options, site): (CommentQuery<'a>, &'a Site)| async move { // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); let local_user_id_join = options .local_user .local_user_id() @@ -245,13 +171,7 @@ fn queries<'a>() -> Queries< query = query.filter(post::community_id.eq(community_id)); } - let is_subscribed = exists( - community_follower::table.filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ); + let is_subscribed = community_actions::followed.is_not_null(); match options.listing_type.unwrap_or_default() { ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ @@ -262,29 +182,27 @@ fn queries<'a>() -> Queries< } ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), ListingType::ModeratorView => { - query = query.filter(exists( - community_moderator::table.filter( - post::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id_join)), - ), - )); + query = query.filter(community_actions::became_moderator.is_not_null()); } } // If its saved only, then filter, and order by the saved time, not the comment creation time. if options.saved_only.unwrap_or_default() { query = query - .filter(comment_saved::person_id.is_not_null()) - .then_order_by(comment_saved::published.desc()); + .filter(comment_actions::saved.is_not_null()) + .then_order_by(comment_actions::saved.desc()); } if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = comment::creator_id.ne(my_id); if options.liked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); + query = query + .filter(not_creator_filter) + .filter(comment_actions::like_score.eq(1)); } else if options.disliked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); + query = query + .filter(not_creator_filter) + .filter(comment_actions::like_score.eq(-1)); } } @@ -305,21 +223,10 @@ fn queries<'a>() -> Queries< )); // Don't show blocked communities or persons - query = query.filter(not(exists( - instance_block::table.filter( - community::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(exists( - community_block::table.filter( - community::id - .eq(community_block::community_id) - .and(community_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(is_creator_blocked(person_id_join))); + query = query + .filter(instance_actions::blocked.is_null()) + .filter(community_actions::blocked.is_null()) + .filter(person_actions::blocked.is_null()); }; if !options.local_user.show_nsfw(site) { @@ -334,15 +241,7 @@ fn queries<'a>() -> Queries< query = query.filter( community::visibility .ne(CommunityVisibility::Private) - .or(exists( - community_follower::table.filter( - post::community_id.eq(community_follower::community_id).and( - community_follower::person_id - .eq(person_id_join) - .and(community_follower::state.eq(CommunityFollowerState::Accepted)), - ), - ), - )), + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index d6577af38..c6c19bf6f 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -10,27 +10,23 @@ use diesel::{ }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{CommunityId, PersonId, PostId, PostReportId}, schema::{ community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, - person_post_aggregates, + person_actions, post, + post_actions, post_aggregates, - post_hide, - post_like, - post_read, post_report, - post_saved, }, source::community::CommunityFollower, utils::{ + actions, + actions_alias, functions::coalesce, get_conn, limit_and_offset, @@ -52,25 +48,16 @@ fn queries<'a>() -> Queries< .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(person::table.on(post_report::creator_id.eq(person::id))) .inner_join(aliases::person1.on(post::creator_id.eq(aliases::person1.field(person::id)))) - .left_join( - community_person_ban::table.on( - post::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(post::creator_id)), - ), - ) - .left_join( - aliases::community_moderator1.on( - aliases::community_moderator1 - .field(community_moderator::community_id) - .eq(post::community_id) - .and( - aliases::community_moderator1 - .field(community_moderator::person_id) - .eq(my_person_id), - ), - ), - ) + .left_join(actions_alias( + creator_community_actions, + post::creator_id, + post::community_id, + )) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) .left_join( local_user::table.on( post::creator_id @@ -78,55 +65,12 @@ fn queries<'a>() -> Queries< .and(local_user::admin.eq(true)), ), ) - .left_join( - post_saved::table.on( - post::id - .eq(post_saved::post_id) - .and(post_saved::person_id.eq(my_person_id)), - ), - ) - .left_join( - post_read::table.on( - post::id - .eq(post_read::post_id) - .and(post_read::person_id.eq(my_person_id)), - ), - ) - .left_join( - post_hide::table.on( - post::id - .eq(post_hide::post_id) - .and(post_hide::person_id.eq(my_person_id)), - ), - ) - .left_join( - person_block::table.on( - post::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), - ), - ) - .left_join( - person_post_aggregates::table.on( - post::id - .eq(person_post_aggregates::post_id) - .and(person_post_aggregates::person_id.eq(my_person_id)), - ), - ) - .left_join( - community_follower::table.on( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(my_person_id)), - ), - ) - .left_join( - post_like::table.on( - post::id - .eq(post_like::post_id) - .and(post_like::person_id.eq(my_person_id)), - ), - ) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + post::creator_id, + )) .inner_join(post_aggregates::table.on(post_report::post_id.eq(post_aggregates::post_id))) .left_join( aliases::person2 @@ -138,20 +82,23 @@ fn queries<'a>() -> Queries< community::all_columns, person::all_columns, aliases::person1.fields(person::all_columns), - community_person_ban::community_id.nullable().is_not_null(), - aliases::community_moderator1 - .field(community_moderator::community_id) + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) .nullable() .is_not_null(), local_user::admin.nullable().is_not_null(), CommunityFollower::select_subscribed_type(), - post_saved::post_id.nullable().is_not_null(), - post_read::post_id.nullable().is_not_null(), - post_hide::post_id.nullable().is_not_null(), - person_block::target_id.nullable().is_not_null(), - post_like::score.nullable(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + post_actions::like_score.nullable(), coalesce( - post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(), + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), post_aggregates::comments, ), post_aggregates::all_columns, @@ -195,19 +142,10 @@ fn queries<'a>() -> Queries< // If its not an admin, get only the ones you mod if !user.local_user.admin { - query - .inner_join( - community_moderator::table.on( - community_moderator::community_id - .eq(post::community_id) - .and(community_moderator::person_id.eq(user.person.id)), - ), - ) - .load::(&mut conn) - .await - } else { - query.load::(&mut conn).await + query = query.filter(community_actions::became_moderator.is_not_null()); } + + query.load::(&mut conn).await }; Queries::new(read, list) @@ -247,10 +185,11 @@ impl PostReportView { if !admin { query .inner_join( - community_moderator::table.on( - community_moderator::community_id + community_actions::table.on( + community_actions::community_id .eq(post::community_id) - .and(community_moderator::person_id.eq(my_person_id)), + .and(community_actions::person_id.eq(my_person_id)) + .and(community_actions::became_moderator.is_not_null()), ), ) .select(count(post_report::id)) diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 4af3a3e2d..e32c79fff 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -5,11 +5,8 @@ use diesel::{ pg::Pg, query_builder::AsQuery, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, OptionalExtension, @@ -20,27 +17,21 @@ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ aggregates::structs::{post_aggregates_keys as key, PostAggregates}, + aliases::creator_community_actions, impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, LocalUserId, PersonId, PostId}, schema::{ community, - community_block, - community_follower, - community_moderator, - community_person_ban, + community_actions, image_details, - instance_block, + instance_actions, local_user, local_user_language, person, - person_block, - person_post_aggregates, + person_actions, post, + post_actions, post_aggregates, - post_hide, - post_like, - post_read, - post_saved, }, source::{ community::{CommunityFollower, CommunityFollowerState}, @@ -48,6 +39,9 @@ use lemmy_db_schema::{ site::Site, }, utils::{ + action_query, + actions, + actions_alias, functions::coalesce, fuzzy_search, get_conn, @@ -72,32 +66,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, PostView, (PostId, Option<&'a LocalUser>, bool)>, impl ListFn<'a, PostView, (PostQuery<'a>, &'a Site)>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - post_aggregates::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(post_aggregates::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - post_aggregates::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - post_aggregates::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(post_aggregates::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( post_aggregates::creator_id @@ -106,155 +74,63 @@ fn queries<'a>() -> Queries< ), ); - let is_read = |person_id| { - exists( - post_read::table.filter( - post_aggregates::post_id - .eq(post_read::post_id) - .and(post_read::person_id.eq(person_id)), - ), - ) - }; - - let is_hidden = |person_id| { - exists( - post_hide::table.filter( - post_aggregates::post_id - .eq(post_hide::post_id) - .and(post_hide::person_id.eq(person_id)), - ), - ) - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - post_aggregates::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - post_like::table - .filter( - post_aggregates::post_id - .eq(post_like::post_id) - .and(post_like::person_id.eq(person_id)), - ) - .select(post_like::score.nullable()) - .single_value() - }; - // TODO maybe this should go to localuser also let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_read_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_read(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_hidden_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_hidden(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression< - _, - Pg, - SqlType = sql_types::Nullable, - >, - > = if let Some(person_id) = my_person_id { - Box::new( - community_follower::table - .filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(CommunityFollower::select_subscribed_type()) - .single_value(), - ) - } else { - Box::new(None::.into_sql::>()) - }; - - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let read_comments: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new( - person_post_aggregates::table - .filter( - post_aggregates::post_id - .eq(person_post_aggregates::post_id) - .and(person_post_aggregates::person_id.eq(person_id)), - ) - .select(person_post_aggregates::read_comments.nullable()) - .single_value(), - ) - } else { - Box::new(None::.into_sql::>()) - }; - query .inner_join(person::table) .inner_join(community::table) .inner_join(post::table) .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) - .left_join( - post_saved::table.on( - post_aggregates::post_id - .eq(post_saved::post_id) - .and(post_saved::person_id.eq(my_person_id.unwrap_or(PersonId(-1)))), - ), - ) + .left_join(actions( + community_actions::table, + my_person_id, + post_aggregates::community_id, + )) + .left_join(actions( + person_actions::table, + my_person_id, + post_aggregates::creator_id, + )) + .left_join(actions( + post_actions::table, + my_person_id, + post_aggregates::post_id, + )) + .left_join(actions( + instance_actions::table, + my_person_id, + post_aggregates::instance_id, + )) + .left_join(actions_alias( + creator_community_actions, + post_aggregates::creator_id, + post_aggregates::community_id, + )) .select(( post::all_columns, person::all_columns, community::all_columns, image_details::all_columns.nullable(), - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, post_aggregates::all_columns, - subscribed_type_selection, - post_saved::person_id.nullable().is_not_null(), - is_read_selection, - is_hidden_selection, - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + post_actions::like_score.nullable(), coalesce( - post_aggregates::comments.nullable() - read_comments, + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), post_aggregates::comments, ), )) @@ -305,17 +181,7 @@ fn queries<'a>() -> Queries< .filter( community::visibility .ne(CommunityVisibility::Private) - .or(exists( - community_follower::table.filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and( - community_follower::person_id - .eq(my_local_user.map(|l| l.person_id).unwrap_or_default()) - .and(community_follower::state.eq(CommunityFollowerState::Accepted)), - ), - ), - )), + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } @@ -329,7 +195,6 @@ fn queries<'a>() -> Queries< let list = move |mut conn: DbConn<'a>, (options, site): (PostQuery<'a>, &'a Site)| async move { // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); let local_user_id_join = options .local_user .local_user_id() @@ -371,13 +236,7 @@ fn queries<'a>() -> Queries< query = query.filter(post_aggregates::creator_id.eq(creator_id)); } - let is_subscribed = exists( - community_follower::table.filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ); + let is_subscribed = community_actions::followed.is_not_null(); match options.listing_type.unwrap_or_default() { ListingType::Subscribed => query = query.filter(is_subscribed), ListingType::Local => { @@ -387,13 +246,7 @@ fn queries<'a>() -> Queries< } ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)), ListingType::ModeratorView => { - query = query.filter(exists( - community_moderator::table.filter( - post::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id_join)), - ), - )); + query = query.filter(community_actions::became_moderator.is_not_null()); } } @@ -434,8 +287,8 @@ fn queries<'a>() -> Queries< // If its saved only, then filter, and order by the saved time, not the comment creation time. if options.saved_only.unwrap_or_default() { query = query - .filter(post_saved::person_id.is_not_null()) - .then_order_by(post_saved::published.desc()); + .filter(post_actions::saved.is_not_null()) + .then_order_by(post_actions::saved.desc()); } // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read // setting wont be able to see saved posts. @@ -445,24 +298,26 @@ fn queries<'a>() -> Queries< { // Do not hide read posts when it is a user profile view // Or, only hide read posts on non-profile views - if let (None, Some(person_id)) = (options.creator_id, options.local_user.person_id()) { - query = query.filter(not(is_read(person_id))); + if options.creator_id.is_none() { + query = query.filter(post_actions::read.is_null()); } } - if !options.show_hidden.unwrap_or_default() { - // If a creator id isn't given (IE its on home or community pages), hide the hidden posts - if let (None, Some(person_id)) = (options.creator_id, options.local_user.person_id()) { - query = query.filter(not(is_hidden(person_id))); - } + // If a creator id isn't given (IE its on home or community pages), hide the hidden posts + if !options.show_hidden.unwrap_or_default() && options.creator_id.is_none() { + query = query.filter(post_actions::hidden.is_null()); } if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = post_aggregates::creator_id.ne(my_id); if options.liked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(1)); + query = query + .filter(not_creator_filter) + .filter(post_actions::like_score.eq(1)); } else if options.disliked_only.unwrap_or_default() { - query = query.filter(not_creator_filter).filter(score(my_id).eq(-1)); + query = query + .filter(not_creator_filter) + .filter(post_actions::like_score.eq(-1)); } }; @@ -472,47 +327,27 @@ fn queries<'a>() -> Queries< query = query.filter( community::visibility .ne(CommunityVisibility::Private) - .or(exists( - community_follower::table.filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)) - .and(community_follower::state.eq(CommunityFollowerState::Accepted)), - ), - )), + .or(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), ); } // Dont filter blocks or missing languages for moderator view type - if let (Some(person_id), false) = ( - options.local_user.person_id(), - options.listing_type.unwrap_or_default() == ListingType::ModeratorView, - ) { - // Filter out the rows with missing languages - query = query.filter(exists( - local_user_language::table.filter( - post::language_id - .eq(local_user_language::language_id) - .and(local_user_language::local_user_id.eq(local_user_id_join)), - ), - )); + if options.listing_type.unwrap_or_default() != ListingType::ModeratorView { + // Filter out the rows with missing languages if user is logged in + if options.local_user.is_some() { + query = query.filter(exists( + local_user_language::table.filter( + post::language_id + .eq(local_user_language::language_id) + .and(local_user_language::local_user_id.eq(local_user_id_join)), + ), + )); + } // Don't show blocked instances, communities or persons - query = query.filter(not(exists( - community_block::table.filter( - post_aggregates::community_id - .eq(community_block::community_id) - .and(community_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(exists( - instance_block::table.filter( - post_aggregates::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(person_id_join)), - ), - ))); - query = query.filter(not(is_creator_blocked(person_id))); + query = query.filter(community_actions::blocked.is_null()); + query = query.filter(instance_actions::blocked.is_null()); + query = query.filter(person_actions::blocked.is_null()); } let (limit, offset) = limit_and_offset(options.page, options.limit)?; @@ -682,13 +517,10 @@ impl<'a> PostQuery<'a> { // covers the "worst case" of the whole page consisting of posts from one community // but using the largest community decreases the pagination-frame so make the real query more // efficient. - use lemmy_db_schema::schema::{ - community_aggregates::dsl::{community_aggregates, community_id, users_active_month}, - community_follower::dsl::{ - community_follower, - community_id as follower_community_id, - person_id, - }, + use lemmy_db_schema::schema::community_aggregates::dsl::{ + community_aggregates, + community_id, + users_active_month, }; let (limit, offset) = limit_and_offset(self.page, self.limit)?; if offset != 0 && self.page_after.is_some() { @@ -699,9 +531,9 @@ impl<'a> PostQuery<'a> { let self_person_id = self.local_user.expect("part of the above if").person_id; let largest_subscribed = { let conn = &mut get_conn(pool).await?; - community_follower - .filter(person_id.eq(self_person_id)) - .inner_join(community_aggregates.on(community_id.eq(follower_community_id))) + action_query(community_actions::followed) + .filter(community_actions::person_id.eq(self_person_id)) + .inner_join(community_aggregates.on(community_id.eq(community_actions::community_id))) .order_by(users_active_month.desc()) .select(community_id) .limit(1) @@ -816,7 +648,7 @@ mod tests { site::Site, }, traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, - utils::{build_db_pool, build_db_pool_for_tests, get_conn, DbPool, RANK_DEFAULT}, + utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, SubscribedType, @@ -1212,7 +1044,7 @@ mod tests { let like_removed = PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id).await?; - assert_eq!(1, like_removed); + assert_eq!(uplete::Count::only_deleted(1), like_removed); cleanup(data, pool).await } diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index 0b9a2708a..2286b7dc6 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -12,8 +12,8 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, newtypes::{PersonId, PrivateMessageId}, - schema::{instance_block, person, person_block, private_message}, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + schema::{instance_actions, person, person_actions, private_message}, + utils::{actions, get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, }; use tracing::debug; @@ -27,20 +27,16 @@ fn queries<'a>() -> Queries< .inner_join( aliases::person1.on(private_message::recipient_id.eq(aliases::person1.field(person::id))), ) - .left_join( - person_block::table.on( - private_message::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(aliases::person1.field(person::id))), - ), - ) - .left_join( - instance_block::table.on( - person::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(aliases::person1.field(person::id))), - ), - ) + .left_join(actions( + person_actions::table, + Some(aliases::person1.field(person::id)), + private_message::creator_id, + )) + .left_join(actions( + instance_actions::table, + Some(aliases::person1.field(person::id)), + person::instance_id, + )) }; let selection = ( @@ -62,9 +58,9 @@ fn queries<'a>() -> Queries< let mut query = all_joins(private_message::table.into_boxed()) .select(selection) // Dont show replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) // Dont show replies from blocked instances - .filter(instance_block::person_id.is_null()); + .filter(instance_actions::blocked.is_null()); // If its unread, I only want the ones to me if options.unread_only { @@ -127,24 +123,20 @@ impl PrivateMessageView { private_message::table // Necessary to get the senders instance_id .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .left_join( - person_block::table.on( - private_message::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(my_person_id)), - ), - ) - .left_join( - instance_block::table.on( - person::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(my_person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(my_person_id), + private_message::creator_id, + )) + .left_join(actions( + instance_actions::table, + Some(my_person_id), + person::instance_id, + )) // Dont count replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) // Dont count replies from blocked instances - .filter(instance_block::person_id.is_null()) + .filter(instance_actions::blocked.is_null()) .filter(private_message::read.eq(false)) .filter(private_message::recipient_id.eq(my_person_id)) .filter(private_message::deleted.eq(false)) diff --git a/crates/db_views/src/vote_view.rs b/crates/db_views/src/vote_view.rs index 79ba7f72a..9af0bd756 100644 --- a/crates/db_views/src/vote_view.rs +++ b/crates/db_views/src/vote_view.rs @@ -1,17 +1,11 @@ use crate::structs::VoteView; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; +use diesel::{result::Error, ExpressionMethods, NullableExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ + aliases::creator_community_actions, newtypes::{CommentId, PostId}, - schema::{comment, comment_like, community_person_ban, person, post, post_like}, - utils::{get_conn, limit_and_offset, DbPool}, + schema::{comment, comment_actions, community_actions, person, post, post_actions}, + utils::{action_query, actions_alias, get_conn, limit_and_offset, DbPool}, }; impl VoteView { @@ -24,24 +18,24 @@ impl VoteView { let conn = &mut get_conn(pool).await?; let (limit, offset) = limit_and_offset(page, limit)?; - post_like::table + action_query(post_actions::like_score) .inner_join(person::table) .inner_join(post::table) - // Join to community_person_ban to get creator_banned_from_community - .left_join( - community_person_ban::table.on( - post::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(post_like::person_id)), - ), - ) - .filter(post_like::post_id.eq(post_id)) + .left_join(actions_alias( + creator_community_actions, + post_actions::person_id, + post::community_id, + )) + .filter(post_actions::post_id.eq(post_id)) .select(( person::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - post_like::score, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + post_actions::like_score.assume_not_null(), )) - .order_by(post_like::score) + .order_by(post_actions::like_score) .limit(limit) .offset(offset) .load::(conn) @@ -57,25 +51,24 @@ impl VoteView { let conn = &mut get_conn(pool).await?; let (limit, offset) = limit_and_offset(page, limit)?; - comment_like::table + action_query(comment_actions::like_score) .inner_join(person::table) - .inner_join(comment::table) - .inner_join(post::table.on(comment::post_id.eq(post::id))) - // Join to community_person_ban to get creator_banned_from_community - .left_join( - community_person_ban::table.on( - post::community_id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment_like::person_id)), - ), - ) - .filter(comment_like::comment_id.eq(comment_id)) + .inner_join(comment::table.inner_join(post::table)) + .left_join(actions_alias( + creator_community_actions, + comment_actions::person_id, + post::community_id, + )) + .filter(comment_actions::comment_id.eq(comment_id)) .select(( person::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - comment_like::score, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + comment_actions::like_score.assume_not_null(), )) - .order_by(comment_like::score) + .order_by(comment_actions::like_score) .limit(limit) .offset(offset) .load::(conn) diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 8694298e0..6c5442e6a 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -3,39 +3,40 @@ use diesel::{ dsl::{exists, not}, pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{CommentReplyId, PersonId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, comment_reply, - comment_saved, community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, + person_actions, post, }, - source::{ - community::{CommunityFollower, CommunityFollowerState}, - local_user::LocalUser, + source::{community::CommunityFollower, local_user::LocalUser}, + utils::{ + actions, + actions_alias, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, }, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, }; @@ -43,74 +44,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, CommentReplyView, (CommentReplyId, Option)>, impl ListFn<'a, CommentReplyView, CommentReplyQuery>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let is_saved = |person_id| { - exists( - comment_saved::table.filter( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(person_id)), - ), - ) - }; - - let is_community_followed = |person_id| { - community_follower::table - .filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(CommunityFollower::select_subscribed_type()) - .single_value() - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - comment_like::table - .filter( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(person_id)), - ) - .select(comment_like::score.nullable()) - .single_value() - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - community::id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(comment::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( comment::creator_id @@ -121,48 +54,6 @@ fn queries<'a>() -> Queries< let all_joins = move |query: comment_reply::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression< - _, - Pg, - SqlType = sql_types::Nullable, - >, - > = if let Some(person_id) = my_person_id { - Box::new(is_community_followed(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let is_saved_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_saved(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - query .inner_join(comment::table) .inner_join(person::table.on(comment::creator_id.eq(person::id))) @@ -170,6 +61,22 @@ fn queries<'a>() -> Queries< .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(aliases::person1) .inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .select(( comment_reply::all_columns, comment::all_columns, @@ -178,14 +85,20 @@ fn queries<'a>() -> Queries< community::all_columns, aliases::person1.fields(person::all_columns), comment_aggregates::all_columns, - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, - subscribed_type_selection, - is_saved_selection, - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), )) }; @@ -228,9 +141,7 @@ fn queries<'a>() -> Queries< }; // Don't show replies from blocked persons - if let Some(my_person_id) = options.my_person_id { - query = query.filter(not(is_creator_blocked(my_person_id))); - } + query = query.filter(person_actions::blocked.is_null()); let (limit, offset) = limit_and_offset(options.page, options.limit)?; @@ -264,13 +175,11 @@ impl CommentReplyView { let mut query = comment_reply::table .inner_join(comment::table) - .left_join( - person_block::table.on( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(local_user.person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(local_user.person_id), + comment::creator_id, + )) .inner_join(person::table.on(comment::creator_id.eq(person::id))) .into_boxed(); @@ -281,7 +190,7 @@ impl CommentReplyView { query // Don't count replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) .filter(comment_reply::recipient_id.eq(local_user.person_id)) .filter(comment_reply::read.eq(false)) .filter(comment::deleted.eq(false)) diff --git a/crates/db_views_actor/src/community_follower_view.rs b/crates/db_views_actor/src/community_follower_view.rs index f9413a078..d3015c182 100644 --- a/crates/db_views_actor/src/community_follower_view.rs +++ b/crates/db_views_actor/src/community_follower_view.rs @@ -12,12 +12,12 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, - schema::{community, community_follower, community_moderator, person}, + schema::{community, community_actions, person}, source::{ community::{Community, CommunityFollower, CommunityFollowerState}, person::Person, }, - utils::{get_conn, limit_and_offset, DbPool}, + utils::{action_query, get_conn, limit_and_offset, DbPool}, CommunityVisibility, SubscribedType, }; @@ -39,14 +39,14 @@ impl CommunityFollowerView { // that would work for all instances that support fully shared inboxes. // It would be a bit more complicated though to keep it in sync. - community_follower::table + community_actions::table .inner_join(community::table) - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .filter(person::instance_id.eq(instance_id)) .filter(community::local) // this should be a no-op since community_followers table only has // local-person+remote-community or remote-person+local-community .filter(not(person::local)) - .filter(community_follower::published.gt(published_since.naive_utc())) + .filter(community_actions::followed.gt(published_since.naive_utc())) .select((community::id, person::inbox_url)) .distinct() // only need each community_id, inbox combination once .load::<(CommunityId, DbUrl)>(conn) @@ -57,10 +57,10 @@ impl CommunityFollowerView { community_id: CommunityId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let res = community_follower::table - .filter(community_follower::community_id.eq(community_id)) + let res = action_query(community_actions::followed) + .filter(community_actions::community_id.eq(community_id)) .filter(not(person::local)) - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .select(person::inbox_url) .distinct() .load::(conn) @@ -73,8 +73,8 @@ impl CommunityFollowerView { community_id: CommunityId, ) -> Result { let conn = &mut get_conn(pool).await?; - let res = community_follower::table - .filter(community_follower::community_id.eq(community_id)) + let res = action_query(community_actions::followed) + .filter(community_actions::community_id.eq(community_id)) .select(count_star()) .first::(conn) .await?; @@ -84,11 +84,11 @@ impl CommunityFollowerView { pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_follower::table + action_query(community_actions::followed) .inner_join(community::table) - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .select((community::all_columns, person::all_columns)) - .filter(community_follower::person_id.eq(person_id)) + .filter(community_actions::person_id.eq(person_id)) .filter(community::deleted.eq(false)) .filter(community::removed.eq(false)) .order_by(community::title) @@ -110,7 +110,7 @@ impl CommunityFollowerView { let (limit, offset) = limit_and_offset(page, limit)?; let (person_alias, community_follower_alias) = diesel::alias!( person as person_alias, - community_follower as community_follower_alias + community_actions as community_follower_alias ); // check if the community already has an accepted follower from the same instance @@ -120,7 +120,7 @@ impl CommunityFollowerView { community_follower_alias.on( person_alias .field(person::id) - .eq(community_follower_alias.field(community_follower::person_id)), + .eq(community_follower_alias.field(community_actions::person_id)), ), ) .filter( @@ -128,36 +128,33 @@ impl CommunityFollowerView { .eq(person_alias.field(person::instance_id)) .and( community_follower_alias - .field(community_follower::community_id) - .eq(community_follower::community_id), + .field(community_actions::community_id) + .eq(community_actions::community_id), ) .and( community_follower_alias - .field(community_follower::state) + .field(community_actions::follow_state) .eq(CommunityFollowerState::Accepted), ), ), )); - let mut query = community_follower::table - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) + let mut query = action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) .inner_join(community::table) .into_boxed(); if all_communities { // if param is false, only return items for communities where user is a mod - query = query.filter(exists( - community_moderator::table.filter( - community_follower::community_id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(person_id)), - ), - )); + query = query + .filter(community_actions::became_moderator.is_not_null()) + .filter(community_actions::person_id.eq(person_id)); } if pending_only { - query = query.filter(community_follower::state.eq(CommunityFollowerState::ApprovalRequired)); + query = + query.filter(community_actions::follow_state.eq(CommunityFollowerState::ApprovalRequired)); } let res = query - .order_by(community_follower::published.asc()) + .order_by(community_actions::followed.asc()) .limit(limit) .offset(offset) .select(( @@ -188,11 +185,11 @@ impl CommunityFollowerView { community_id: CommunityId, ) -> Result { let conn = &mut get_conn(pool).await?; - community_follower::table - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) - .filter(community_follower::community_id.eq(community_id)) - .filter(community_follower::state.eq(CommunityFollowerState::ApprovalRequired)) - .select(count(community_follower::community_id)) + action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + .filter(community_actions::community_id.eq(community_id)) + .filter(community_actions::follow_state.eq(CommunityFollowerState::ApprovalRequired)) + .select(count(community_actions::community_id)) .first::(conn) .await } @@ -206,10 +203,10 @@ impl CommunityFollowerView { } let conn = &mut get_conn(pool).await?; select(exists( - community_follower::table - .filter(community_follower::community_id.eq(community.id)) - .filter(community_follower::person_id.eq(from_person_id)) - .filter(community_follower::state.eq(CommunityFollowerState::Accepted)), + action_query(community_actions::followed) + .filter(community_actions::community_id.eq(community.id)) + .filter(community_actions::person_id.eq(from_person_id)) + .filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), )) .get_result::(conn) .await? @@ -223,11 +220,11 @@ impl CommunityFollowerView { ) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; select(exists( - community_follower::table - .inner_join(person::table.on(community_follower::person_id.eq(person::id))) - .filter(community_follower::community_id.eq(community_id)) + action_query(community_actions::followed) + .inner_join(person::table.on(community_actions::person_id.eq(person::id))) + .filter(community_actions::community_id.eq(community_id)) .filter(person::instance_id.eq(instance_id)) - .filter(community_follower::state.eq(CommunityFollowerState::Accepted)), + .filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)), )) .get_result::(conn) .await? diff --git a/crates/db_views_actor/src/community_moderator_view.rs b/crates/db_views_actor/src/community_moderator_view.rs index 7126af1f6..a9ada92e1 100644 --- a/crates/db_views_actor/src/community_moderator_view.rs +++ b/crates/db_views_actor/src/community_moderator_view.rs @@ -1,12 +1,12 @@ use crate::structs::CommunityModeratorView; -use diesel::{dsl::exists, result::Error, select, ExpressionMethods, QueryDsl}; +use diesel::{dsl::exists, result::Error, select, ExpressionMethods, JoinOnDsl, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, - schema::{community, community_moderator, person}, + schema::{community, community_actions, person}, source::local_user::LocalUser, - utils::{get_conn, DbPool}, + utils::{action_query, find_action, get_conn, DbPool}, }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -16,17 +16,11 @@ impl CommunityModeratorView { find_community_id: CommunityId, find_person_id: PersonId, ) -> LemmyResult<()> { - use lemmy_db_schema::schema::community_moderator::dsl::{ - community_id, - community_moderator, - person_id, - }; let conn = &mut get_conn(pool).await?; - select(exists( - community_moderator - .filter(community_id.eq(find_community_id)) - .filter(person_id.eq(find_person_id)), - )) + select(exists(find_action( + community_actions::became_moderator, + (find_person_id, find_community_id), + ))) .get_result::(conn) .await? .then_some(()) @@ -37,10 +31,10 @@ impl CommunityModeratorView { pool: &mut DbPool<'_>, find_person_id: PersonId, ) -> LemmyResult<()> { - use lemmy_db_schema::schema::community_moderator::dsl::{community_moderator, person_id}; let conn = &mut get_conn(pool).await?; select(exists( - community_moderator.filter(person_id.eq(find_person_id)), + action_query(community_actions::became_moderator) + .filter(community_actions::person_id.eq(find_person_id)), )) .get_result::(conn) .await? @@ -53,12 +47,12 @@ impl CommunityModeratorView { community_id: CommunityId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_moderator::table + action_query(community_actions::became_moderator) .inner_join(community::table) - .inner_join(person::table) - .filter(community_moderator::community_id.eq(community_id)) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) + .filter(community_actions::community_id.eq(community_id)) .select((community::all_columns, person::all_columns)) - .order_by(community_moderator::published) + .order_by(community_actions::became_moderator) .load::(conn) .await } @@ -69,10 +63,10 @@ impl CommunityModeratorView { local_user: Option<&LocalUser>, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let mut query = community_moderator::table + let mut query = action_query(community_actions::became_moderator) .inner_join(community::table) - .inner_join(person::table) - .filter(community_moderator::person_id.eq(person_id)) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) + .filter(community_actions::person_id.eq(person_id)) .select((community::all_columns, person::all_columns)) .into_boxed(); @@ -95,16 +89,16 @@ impl CommunityModeratorView { /// Ideally this should be a group by, but diesel doesn't support it yet pub async fn get_community_first_mods(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; - community_moderator::table + action_query(community_actions::became_moderator) .inner_join(community::table) - .inner_join(person::table) + .inner_join(person::table.on(person::id.eq(community_actions::person_id))) .select((community::all_columns, person::all_columns)) // A hacky workaround instead of group_bys // https://stackoverflow.com/questions/24042359/how-to-join-only-one-row-in-joined-table-with-postgres - .distinct_on(community_moderator::community_id) + .distinct_on(community_actions::community_id) .order_by(( - community_moderator::community_id, - community_moderator::published, + community_actions::community_id, + community_actions::became_moderator, )) .load::(conn) .await diff --git a/crates/db_views_actor/src/community_person_ban_view.rs b/crates/db_views_actor/src/community_person_ban_view.rs index 9bfa0704c..224ea8d53 100644 --- a/crates/db_views_actor/src/community_person_ban_view.rs +++ b/crates/db_views_actor/src/community_person_ban_view.rs @@ -2,14 +2,12 @@ use crate::structs::CommunityPersonBanView; use diesel::{ dsl::{exists, not}, select, - ExpressionMethods, - QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{CommunityId, PersonId}, - schema::community_person_ban, - utils::{get_conn, DbPool}, + schema::community_actions, + utils::{find_action, get_conn, DbPool}, }; use lemmy_utils::error::{LemmyErrorType, LemmyResult}; @@ -20,11 +18,10 @@ impl CommunityPersonBanView { from_community_id: CommunityId, ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - select(not(exists( - community_person_ban::table - .filter(community_person_ban::community_id.eq(from_community_id)) - .filter(community_person_ban::person_id.eq(from_person_id)), - ))) + select(not(exists(find_action( + community_actions::received_ban, + (from_person_id, from_community_id), + )))) .get_result::(conn) .await? .then_some(()) diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 999ec23f0..f42340bdb 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -4,7 +4,6 @@ use diesel::{ result::Error, BoolExpressionMethods, ExpressionMethods, - JoinOnDsl, NullableExpressionMethods, PgTextExpressionMethods, QueryDsl, @@ -13,20 +12,14 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ impls::local_user::LocalUserOptionHelper, newtypes::{CommunityId, PersonId}, - schema::{ - community, - community_aggregates, - community_block, - community_follower, - community_person_ban, - instance_block, - }, + schema::{community, community_actions, community_aggregates, instance_actions}, source::{ community::{CommunityFollower, CommunityFollowerState}, local_user::LocalUser, site::Site, }, utils::{ + actions, functions::lower, fuzzy_search, limit_and_offset, @@ -46,47 +39,26 @@ fn queries<'a>() -> Queries< impl ListFn<'a, CommunityView, (CommunityQuery<'a>, &'a Site)>, > { let all_joins = |query: community::BoxedQuery<'a, Pg>, my_local_user: Option<&'a LocalUser>| { - // The left join below will return None in this case - let person_id_join = my_local_user.person_id().unwrap_or(PersonId(-1)); - query .inner_join(community_aggregates::table) - .left_join( - community_follower::table.on( - community::id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id_join)), - ), - ) - .left_join( - instance_block::table.on( - community::instance_id - .eq(instance_block::instance_id) - .and(instance_block::person_id.eq(person_id_join)), - ), - ) - .left_join( - community_block::table.on( - community::id - .eq(community_block::community_id) - .and(community_block::person_id.eq(person_id_join)), - ), - ) - .left_join( - community_person_ban::table.on( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id_join)), - ), - ) + .left_join(actions( + community_actions::table, + my_local_user.person_id(), + community::id, + )) + .left_join(actions( + instance_actions::table, + my_local_user.person_id(), + community::instance_id, + )) }; let selection = ( community::all_columns, CommunityFollower::select_subscribed_type(), - community_block::community_id.nullable().is_not_null(), + community_actions::blocked.nullable().is_not_null(), community_aggregates::all_columns, - community_person_ban::person_id.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), ); let not_removed_or_deleted = community::removed @@ -118,9 +90,6 @@ fn queries<'a>() -> Queries< let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { use CommunitySortType::*; - // The left join below will return None in this case - let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); - let mut query = all_joins(community::table.into_boxed(), options.local_user).select(selection); if let Some(search_term) = options.search_term { @@ -140,7 +109,7 @@ fn queries<'a>() -> Queries< query = query.filter(not_removed_or_deleted).filter( community::hidden .eq(false) - .or(community_follower::person_id.eq(person_id_join)), + .or(community_actions::follow_state.is_not_null()), ); } @@ -168,7 +137,7 @@ fn queries<'a>() -> Queries< if let Some(listing_type) = options.listing_type { query = match listing_type { ListingType::Subscribed => { - query.filter(community_follower::state.eq(CommunityFollowerState::Accepted)) + query.filter(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted))) } ListingType::Local => query.filter(community::local.eq(true)), _ => query, @@ -177,8 +146,8 @@ fn queries<'a>() -> Queries< // Don't show blocked communities and communities on blocked instances. nsfw communities are // also hidden (based on profile setting) - query = query.filter(instance_block::person_id.is_null()); - query = query.filter(community_block::person_id.is_null()); + query = query.filter(instance_actions::blocked.is_null()); + query = query.filter(community_actions::blocked.is_null()); if !(options.local_user.show_nsfw(site) || options.show_nsfw) { query = query.filter(community::nsfw.eq(false)); } diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index 2bc701805..08be67a82 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_mention_view.rs @@ -3,39 +3,40 @@ use diesel::{ dsl::{exists, not}, pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, - IntoSql, JoinOnDsl, NullableExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - aliases, + aliases::{self, creator_community_actions}, newtypes::{PersonId, PersonMentionId}, schema::{ comment, + comment_actions, comment_aggregates, - comment_like, - comment_saved, community, - community_follower, - community_moderator, - community_person_ban, + community_actions, local_user, person, - person_block, + person_actions, person_mention, post, }, - source::{ - community::{CommunityFollower, CommunityFollowerState}, - local_user::LocalUser, + source::{community::CommunityFollower, local_user::LocalUser}, + utils::{ + actions, + actions_alias, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, }, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommentSortType, }; @@ -43,74 +44,6 @@ fn queries<'a>() -> Queries< impl ReadFn<'a, PersonMentionView, (PersonMentionId, Option)>, impl ListFn<'a, PersonMentionView, PersonMentionQuery>, > { - let is_creator_banned_from_community = exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ); - - let is_local_user_banned_from_community = |person_id| { - exists( - community_person_ban::table.filter( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(person_id)), - ), - ) - }; - - let is_saved = |person_id| { - exists( - comment_saved::table.filter( - comment::id - .eq(comment_saved::comment_id) - .and(comment_saved::person_id.eq(person_id)), - ), - ) - }; - - let is_community_followed = |person_id| { - community_follower::table - .filter( - post::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(CommunityFollower::select_subscribed_type()) - .single_value() - }; - - let is_creator_blocked = |person_id| { - exists( - person_block::table.filter( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(person_id)), - ), - ) - }; - - let score = |person_id| { - comment_like::table - .filter( - comment::id - .eq(comment_like::comment_id) - .and(comment_like::person_id.eq(person_id)), - ) - .select(comment_like::score.nullable()) - .single_value() - }; - - let creator_is_moderator = exists( - community_moderator::table.filter( - community::id - .eq(community_moderator::community_id) - .and(community_moderator::person_id.eq(comment::creator_id)), - ), - ); - let creator_is_admin = exists( local_user::table.filter( comment::creator_id @@ -121,47 +54,6 @@ fn queries<'a>() -> Queries< let all_joins = move |query: person_mention::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression< - _, - Pg, - SqlType = sql_types::Nullable, - >, - > = if let Some(person_id) = my_person_id { - Box::new(is_community_followed(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let is_saved_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_saved(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - query .inner_join(comment::table) .inner_join(person::table.on(comment::creator_id.eq(person::id))) @@ -169,6 +61,22 @@ fn queries<'a>() -> Queries< .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(aliases::person1) .inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) .select(( person_mention::all_columns, comment::all_columns, @@ -177,14 +85,20 @@ fn queries<'a>() -> Queries< community::all_columns, aliases::person1.fields(person::all_columns), comment_aggregates::all_columns, - is_creator_banned_from_community, - is_local_user_banned_from_community_selection, - creator_is_moderator, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), creator_is_admin, - subscribed_type_selection, - is_saved_selection, - is_creator_blocked_selection, - score_selection, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), )) }; @@ -227,9 +141,7 @@ fn queries<'a>() -> Queries< }; // Don't show mentions from blocked persons - if let Some(my_person_id) = options.my_person_id { - query = query.filter(not(is_creator_blocked(my_person_id))); - } + query = query.filter(person_actions::blocked.is_null()); let (limit, offset) = limit_and_offset(options.page, options.limit)?; @@ -264,13 +176,11 @@ impl PersonMentionView { let mut query = person_mention::table .inner_join(comment::table) - .left_join( - person_block::table.on( - comment::creator_id - .eq(person_block::target_id) - .and(person_block::person_id.eq(local_user.person_id)), - ), - ) + .left_join(actions( + person_actions::table, + Some(local_user.person_id), + comment::creator_id, + )) .inner_join(person::table.on(comment::creator_id.eq(person::id))) .into_boxed(); @@ -281,7 +191,7 @@ impl PersonMentionView { query // Don't count replies from blocked users - .filter(person_block::person_id.is_null()) + .filter(person_actions::blocked.is_null()) .filter(person_mention::recipient_id.eq(local_user.person_id)) .filter(person_mention::read.eq(false)) .filter(comment::deleted.eq(false)) diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index f8da6f609..43aae8599 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -85,26 +85,19 @@ fn has_newline(name: &str) -> bool { } pub fn is_valid_actor_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> { - static VALID_ACTOR_NAME_REGEX_EN: LazyLock = - LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,}$").expect("compile regex")); - static VALID_ACTOR_NAME_REGEX_AR: LazyLock = - LazyLock::new(|| Regex::new(r"^[\p{Arabic}0-9_]{3,}$").expect("compile regex")); - static VALID_ACTOR_NAME_REGEX_RU: LazyLock = - LazyLock::new(|| Regex::new(r"^[\p{Cyrillic}0-9_]{3,}$").expect("compile regex")); - - let check = name.chars().count() <= actor_name_max_length && !has_newline(name); - // Only allow characters from a single alphabet per username. This avoids problems with lookalike // characters like `o` which looks identical in Latin and Cyrillic, and can be used to imitate // other users. Checks for additional alphabets can be added in the same way. - let lang_check = VALID_ACTOR_NAME_REGEX_EN.is_match(name) - || VALID_ACTOR_NAME_REGEX_AR.is_match(name) - || VALID_ACTOR_NAME_REGEX_RU.is_match(name); + static VALID_ACTOR_NAME_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^(?:[a-zA-Z0-9_]+|[0-9_\p{Arabic}]+|[0-9_\p{Cyrillic}]+)$").expect("compile regex") + }); - if !check || !lang_check { - Err(LemmyErrorType::InvalidName.into()) - } else { + min_length_check(name, 3, LemmyErrorType::InvalidName)?; + max_length_check(name, actor_name_max_length, LemmyErrorType::InvalidName)?; + if VALID_ACTOR_NAME_REGEX.is_match(name) { Ok(()) + } else { + Err(LemmyErrorType::InvalidName.into()) } } @@ -379,11 +372,14 @@ mod tests { use pretty_assertions::assert_eq; use url::Url; + const URL_WITH_TRACKING: &str = "https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123"; + const URL_TRACKING_REMOVED: &str = "https://example.com/path/123?user+name=random+user&id=123"; + #[test] fn test_clean_url_params() -> LemmyResult<()> { - let url = Url::parse("https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123")?; + let url = Url::parse(URL_WITH_TRACKING)?; let cleaned = clean_url(&url); - let expected = Url::parse("https://example.com/path/123?user+name=random+user&id=123")?; + let expected = Url::parse(URL_TRACKING_REMOVED)?; assert_eq!(expected.to_string(), cleaned.to_string()); let url = Url::parse("https://example.com/path/123")?; @@ -395,9 +391,9 @@ mod tests { #[test] fn test_clean_body() -> LemmyResult<()> { - let text = "[a link](https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123)"; - let cleaned = clean_urls_in_text(text); - let expected = "[a link](https://example.com/path/123?user+name=random+user&id=123)"; + let text = format!("[a link]({URL_WITH_TRACKING})"); + let cleaned = clean_urls_in_text(&text); + let expected = format!("[a link]({URL_TRACKING_REMOVED})"); assert_eq!(expected.to_string(), cleaned.to_string()); let text = "[a link](https://example.com/path/123)"; @@ -438,6 +434,15 @@ mod tests { assert!(is_valid_actor_name("a", actor_name_max_length).is_err()); // empty assert!(is_valid_actor_name("", actor_name_max_length).is_err()); + // newline + assert!(is_valid_actor_name( + r"Line1 + +Line3", + actor_name_max_length + ) + .is_err()); + assert!(is_valid_actor_name("Line1\nLine3", actor_name_max_length).is_err()); } #[test] diff --git a/migrations/2024-11-10-134311_smoosh-tables-together/down.sql b/migrations/2024-11-10-134311_smoosh-tables-together/down.sql new file mode 100644 index 000000000..29b95d2cd --- /dev/null +++ b/migrations/2024-11-10-134311_smoosh-tables-together/down.sql @@ -0,0 +1,320 @@ +-- For each new actions table, create tables that are dropped in up.sql, and insert into them +CREATE TABLE comment_saved ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + comment_id int REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, comment_id) +); + +INSERT INTO comment_saved (person_id, comment_id, published) +SELECT + person_id, + comment_id, + saved +FROM + comment_actions +WHERE + saved IS NOT NULL; + +CREATE TABLE community_block ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, community_id) +); + +INSERT INTO community_block (person_id, community_id, published) +SELECT + person_id, + community_id, + blocked +FROM + community_actions +WHERE + blocked IS NOT NULL; + +CREATE TABLE community_person_ban ( + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + expires timestamptz, + PRIMARY KEY (person_id, community_id) +); + +INSERT INTO community_person_ban (community_id, person_id, published, expires) +SELECT + community_id, + person_id, + received_ban, + ban_expires +FROM + community_actions +WHERE + received_ban IS NOT NULL; + +CREATE TABLE community_moderator ( + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, community_id) +); + +INSERT INTO community_moderator (community_id, person_id, published) +SELECT + community_id, + person_id, + became_moderator +FROM + community_actions +WHERE + became_moderator IS NOT NULL; + +CREATE TABLE person_block ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + target_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, target_id) +); + +INSERT INTO person_block (person_id, target_id, published) +SELECT + person_id, + target_id, + blocked +FROM + person_actions +WHERE + blocked IS NOT NULL; + +CREATE TABLE person_post_aggregates ( + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + read_comments bigint DEFAULT 0 NOT NULL, + published timestamptz NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO person_post_aggregates (person_id, post_id, read_comments, published) +SELECT + person_id, + post_id, + read_comments_amount, + read_comments +FROM + post_actions +WHERE + read_comments IS NOT NULL; + +CREATE TABLE post_hide ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO post_hide (post_id, person_id, published) +SELECT + post_id, + person_id, + hidden +FROM + post_actions +WHERE + hidden IS NOT NULL; + +CREATE TABLE post_like ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + score smallint NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO post_like (post_id, person_id, score, published) +SELECT + post_id, + person_id, + like_score, + liked +FROM + post_actions +WHERE + liked IS NOT NULL; + +CREATE TABLE post_saved ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamptz DEFAULT now() NOT NULL, + PRIMARY KEY (person_id, post_id) +); + +INSERT INTO post_saved (post_id, person_id, published) +SELECT + post_id, + person_id, + saved +FROM + post_actions +WHERE + saved IS NOT NULL; + +-- Do the opposite of the `ALTER TABLE` commands in up.sql +DELETE FROM comment_actions +WHERE liked IS NULL; + +DELETE FROM community_actions +WHERE followed IS NULL; + +DELETE FROM instance_actions +WHERE blocked IS NULL; + +DELETE FROM person_actions +WHERE followed IS NULL; + +DELETE FROM post_actions +WHERE read IS NULL; + +ALTER TABLE comment_actions RENAME TO comment_like; + +ALTER TABLE community_actions RENAME TO community_follower; + +ALTER TABLE instance_actions RENAME TO instance_block; + +ALTER TABLE person_actions RENAME TO person_follower; + +ALTER TABLE post_actions RENAME TO post_read; + +ALTER TABLE comment_like RENAME COLUMN liked TO published; + +ALTER TABLE comment_like RENAME COLUMN like_score TO score; + +ALTER TABLE community_follower RENAME COLUMN followed TO published; + +ALTER TABLE community_follower RENAME COLUMN follow_state TO state; + +ALTER TABLE community_follower RENAME COLUMN follow_approver_id TO approver_id; + +ALTER TABLE instance_block RENAME COLUMN blocked TO published; + +ALTER TABLE person_follower RENAME COLUMN person_id TO follower_id; + +ALTER TABLE person_follower RENAME COLUMN target_id TO person_id; + +ALTER TABLE person_follower RENAME COLUMN followed TO published; + +ALTER TABLE person_follower RENAME COLUMN follow_pending TO pending; + +ALTER TABLE post_read RENAME COLUMN read TO published; + +ALTER TABLE comment_like + DROP CONSTRAINT comment_actions_check_liked, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + ALTER COLUMN score SET NOT NULL, + DROP COLUMN saved; + +ALTER TABLE community_follower + DROP CONSTRAINT community_actions_check_followed, + DROP CONSTRAINT community_actions_check_received_ban, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + ALTER COLUMN state SET NOT NULL, + DROP COLUMN blocked, + DROP COLUMN became_moderator, + DROP COLUMN received_ban, + DROP COLUMN ban_expires; + +ALTER TABLE instance_block + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(); + +ALTER TABLE person_follower + DROP CONSTRAINT person_actions_check_followed, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + ALTER COLUMN pending SET NOT NULL, + DROP COLUMN blocked; + +ALTER TABLE post_read + DROP CONSTRAINT post_actions_check_read_comments, + DROP CONSTRAINT post_actions_check_liked, + ALTER COLUMN published SET NOT NULL, + ALTER COLUMN published SET DEFAULT now(), + DROP COLUMN read_comments, + DROP COLUMN read_comments_amount, + DROP COLUMN saved, + DROP COLUMN liked, + DROP COLUMN like_score, + DROP COLUMN hidden; + +-- Rename associated stuff +ALTER INDEX comment_actions_pkey RENAME TO comment_like_pkey; + +ALTER INDEX idx_comment_actions_comment RENAME TO idx_comment_like_comment; + +ALTER TABLE comment_like RENAME CONSTRAINT comment_actions_comment_id_fkey TO comment_like_comment_id_fkey; + +ALTER TABLE comment_like RENAME CONSTRAINT comment_actions_person_id_fkey TO comment_like_person_id_fkey; + +ALTER INDEX community_actions_pkey RENAME TO community_follower_pkey; + +ALTER INDEX idx_community_actions_community RENAME TO idx_community_follower_community; + +ALTER TABLE community_follower RENAME CONSTRAINT community_actions_community_id_fkey TO community_follower_community_id_fkey; + +ALTER TABLE community_follower RENAME CONSTRAINT community_actions_person_id_fkey TO community_follower_person_id_fkey; + +ALTER TABLE community_follower RENAME CONSTRAINT community_actions_follow_approver_id_fkey TO community_follower_approver_id_fkey; + +ALTER INDEX instance_actions_pkey RENAME TO instance_block_pkey; + +ALTER TABLE instance_block RENAME CONSTRAINT instance_actions_instance_id_fkey TO instance_block_instance_id_fkey; + +ALTER TABLE instance_block RENAME CONSTRAINT instance_actions_person_id_fkey TO instance_block_person_id_fkey; + +ALTER INDEX person_actions_pkey RENAME TO person_follower_pkey; + +ALTER TABLE person_follower RENAME CONSTRAINT person_actions_target_id_fkey TO person_follower_person_id_fkey; + +ALTER TABLE person_follower RENAME CONSTRAINT person_actions_person_id_fkey TO person_follower_follower_id_fkey; + +ALTER INDEX post_actions_pkey RENAME TO post_read_pkey; + +ALTER TABLE post_read RENAME CONSTRAINT post_actions_person_id_fkey TO post_read_person_id_fkey; + +ALTER TABLE post_read RENAME CONSTRAINT post_actions_post_id_fkey TO post_read_post_id_fkey; + +-- Rename idx_community_actions_followed and remove filter +CREATE INDEX idx_community_follower_published ON community_follower (published); + +DROP INDEX idx_community_actions_followed; + +-- Move indexes back to their original tables +CREATE INDEX idx_comment_saved_comment ON comment_saved (comment_id); + +CREATE INDEX idx_comment_saved_person ON comment_saved (person_id); + +CREATE INDEX idx_community_block_community ON community_block (community_id); + +CREATE INDEX idx_community_moderator_community ON community_moderator (community_id); + +CREATE INDEX idx_community_moderator_published ON community_moderator (published); + +CREATE INDEX idx_person_block_person ON person_block (person_id); + +CREATE INDEX idx_person_block_target ON person_block (target_id); + +CREATE INDEX idx_person_post_aggregates_person ON person_post_aggregates (person_id); + +CREATE INDEX idx_person_post_aggregates_post ON person_post_aggregates (post_id); + +CREATE INDEX idx_post_like_post ON post_like (post_id); + +DROP INDEX idx_person_actions_person, idx_person_actions_target, idx_post_actions_person, idx_post_actions_post; + +-- Drop `NOT NULL` indexes of columns that still exist +DROP INDEX idx_comment_actions_liked_not_null, idx_community_actions_followed_not_null, idx_person_actions_followed_not_null, idx_post_actions_read_not_null, idx_instance_actions_blocked_not_null; + +-- Drop statistics of columns that still exist +DROP statistics comment_actions_liked_stat, community_actions_followed_stat, person_actions_followed_stat; + diff --git a/migrations/2024-11-10-134311_smoosh-tables-together/up.sql b/migrations/2024-11-10-134311_smoosh-tables-together/up.sql new file mode 100644 index 000000000..aadf95692 --- /dev/null +++ b/migrations/2024-11-10-134311_smoosh-tables-together/up.sql @@ -0,0 +1,338 @@ +-- For each new actions table, transform the table previously used for the most common action type +-- into the new actions table, which should only change the table's metadata instead of rewriting the +-- rows +ALTER TABLE comment_like RENAME TO comment_actions; + +ALTER TABLE community_follower RENAME TO community_actions; + +ALTER TABLE instance_block RENAME TO instance_actions; + +ALTER TABLE person_follower RENAME TO person_actions; + +ALTER TABLE post_read RENAME TO post_actions; + +ALTER TABLE comment_actions RENAME COLUMN published TO liked; + +ALTER TABLE comment_actions RENAME COLUMN score TO like_score; + +ALTER TABLE community_actions RENAME COLUMN published TO followed; + +ALTER TABLE community_actions RENAME COLUMN state TO follow_state; + +ALTER TABLE community_actions RENAME COLUMN approver_id TO follow_approver_id; + +ALTER TABLE instance_actions RENAME COLUMN published TO blocked; + +ALTER TABLE person_actions RENAME COLUMN person_id TO target_id; + +ALTER TABLE person_actions RENAME COLUMN follower_id TO person_id; + +ALTER TABLE person_actions RENAME COLUMN published TO followed; + +ALTER TABLE person_actions RENAME COLUMN pending TO follow_pending; + +ALTER TABLE post_actions RENAME COLUMN published TO read; + +ALTER TABLE comment_actions + ALTER COLUMN liked DROP NOT NULL, + ALTER COLUMN liked DROP DEFAULT, + ALTER COLUMN like_score DROP NOT NULL, + ADD COLUMN saved timestamptz, + ADD CONSTRAINT comment_actions_check_liked CHECK ((liked IS NULL) = (like_score IS NULL)); + +ALTER TABLE community_actions + ALTER COLUMN followed DROP NOT NULL, + ALTER COLUMN followed DROP DEFAULT, + ALTER COLUMN follow_state DROP NOT NULL, + ADD COLUMN blocked timestamptz, + ADD COLUMN became_moderator timestamptz, + ADD COLUMN received_ban timestamptz, + ADD COLUMN ban_expires timestamptz, + ADD CONSTRAINT community_actions_check_followed CHECK ((followed IS NULL) = (follow_state IS NULL) AND NOT (followed IS NULL AND follow_approver_id IS NOT NULL)), + ADD CONSTRAINT community_actions_check_received_ban CHECK (NOT (received_ban IS NULL AND ban_expires IS NOT NULL)); + +ALTER TABLE instance_actions + ALTER COLUMN blocked DROP NOT NULL, + ALTER COLUMN blocked DROP DEFAULT; + +ALTER TABLE person_actions + ALTER COLUMN followed DROP NOT NULL, + ALTER COLUMN followed DROP DEFAULT, + ALTER COLUMN follow_pending DROP NOT NULL, + ADD COLUMN blocked timestamptz, + ADD CONSTRAINT person_actions_check_followed CHECK ((followed IS NULL) = (follow_pending IS NULL)); + +ALTER TABLE post_actions + ALTER COLUMN read DROP NOT NULL, + ALTER COLUMN read DROP DEFAULT, + ADD COLUMN read_comments timestamptz, + ADD COLUMN read_comments_amount bigint, + ADD COLUMN saved timestamptz, + ADD COLUMN liked timestamptz, + ADD COLUMN like_score smallint, + ADD COLUMN hidden timestamptz, + ADD CONSTRAINT post_actions_check_read_comments CHECK ((read_comments IS NULL) = (read_comments_amount IS NULL)), + ADD CONSTRAINT post_actions_check_liked CHECK ((liked IS NULL) = (like_score IS NULL)); + +-- Add actions from other old tables to the new tables +INSERT INTO comment_actions (person_id, comment_id, saved) +SELECT + person_id, + comment_id, + published +FROM + comment_saved +ON CONFLICT (person_id, + comment_id) + DO UPDATE SET + saved = excluded.saved; + +INSERT INTO community_actions (person_id, community_id, blocked) +SELECT + person_id, + community_id, + published +FROM + community_block +ON CONFLICT (person_id, + community_id) + DO UPDATE SET + person_id = excluded.person_id, + community_id = excluded.community_id, + blocked = excluded.blocked; + +INSERT INTO community_actions (person_id, community_id, became_moderator) +SELECT + person_id, + community_id, + published +FROM + community_moderator +ON CONFLICT (person_id, + community_id) + DO UPDATE SET + person_id = excluded.person_id, + community_id = excluded.community_id, + became_moderator = excluded.became_moderator; + +INSERT INTO community_actions (person_id, community_id, received_ban, ban_expires) +SELECT + person_id, + community_id, + published, + expires +FROM + community_person_ban +ON CONFLICT (person_id, + community_id) + DO UPDATE SET + person_id = excluded.person_id, + community_id = excluded.community_id, + received_ban = excluded.received_ban, + ban_expires = excluded.ban_expires; + +INSERT INTO person_actions (person_id, target_id, blocked) +SELECT + person_id, + target_id, + published +FROM + person_block +ON CONFLICT (person_id, + target_id) + DO UPDATE SET + person_id = excluded.person_id, + target_id = excluded.target_id, + blocked = excluded.blocked; + +INSERT INTO post_actions (person_id, post_id, read_comments, read_comments_amount) +SELECT + person_id, + post_id, + published, + read_comments +FROM + person_post_aggregates +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + read_comments = excluded.read_comments, + read_comments_amount = excluded.read_comments_amount; + +INSERT INTO post_actions (person_id, post_id, hidden) +SELECT + person_id, + post_id, + published +FROM + post_hide +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + hidden = excluded.hidden; + +INSERT INTO post_actions (person_id, post_id, liked, like_score) +SELECT + person_id, + post_id, + published, + score +FROM + post_like +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + liked = excluded.liked, + like_score = excluded.like_score; + +INSERT INTO post_actions (person_id, post_id, saved) +SELECT + person_id, + post_id, + published +FROM + post_saved +ON CONFLICT (person_id, + post_id) + DO UPDATE SET + saved = excluded.saved; + +-- Drop old tables +DROP TABLE comment_saved, community_block, community_moderator, community_person_ban, person_block, person_post_aggregates, post_hide, post_like, post_saved; + +-- Rename associated stuff +ALTER INDEX comment_like_pkey RENAME TO comment_actions_pkey; + +ALTER INDEX idx_comment_like_comment RENAME TO idx_comment_actions_comment; + +ALTER TABLE comment_actions RENAME CONSTRAINT comment_like_comment_id_fkey TO comment_actions_comment_id_fkey; + +ALTER TABLE comment_actions RENAME CONSTRAINT comment_like_person_id_fkey TO comment_actions_person_id_fkey; + +ALTER INDEX community_follower_pkey RENAME TO community_actions_pkey; + +ALTER INDEX idx_community_follower_community RENAME TO idx_community_actions_community; + +ALTER TABLE community_actions RENAME CONSTRAINT community_follower_community_id_fkey TO community_actions_community_id_fkey; + +ALTER TABLE community_actions RENAME CONSTRAINT community_follower_person_id_fkey TO community_actions_person_id_fkey; + +ALTER TABLE community_actions RENAME CONSTRAINT community_follower_approver_id_fkey TO community_actions_follow_approver_id_fkey; + +ALTER INDEX instance_block_pkey RENAME TO instance_actions_pkey; + +ALTER TABLE instance_actions RENAME CONSTRAINT instance_block_instance_id_fkey TO instance_actions_instance_id_fkey; + +ALTER TABLE instance_actions RENAME CONSTRAINT instance_block_person_id_fkey TO instance_actions_person_id_fkey; + +ALTER INDEX person_follower_pkey RENAME TO person_actions_pkey; + +ALTER TABLE person_actions RENAME CONSTRAINT person_follower_person_id_fkey TO person_actions_target_id_fkey; + +ALTER TABLE person_actions RENAME CONSTRAINT person_follower_follower_id_fkey TO person_actions_person_id_fkey; + +ALTER INDEX post_read_pkey RENAME TO post_actions_pkey; + +ALTER TABLE post_actions RENAME CONSTRAINT post_read_person_id_fkey TO post_actions_person_id_fkey; + +ALTER TABLE post_actions RENAME CONSTRAINT post_read_post_id_fkey TO post_actions_post_id_fkey; + +-- Rename idx_community_follower_published and add filter +CREATE INDEX idx_community_actions_followed ON community_actions (followed) +WHERE + followed IS NOT NULL; + +DROP INDEX idx_community_follower_published; + +-- Restore indexes of dropped tables +CREATE INDEX idx_community_actions_became_moderator ON community_actions (became_moderator) +WHERE + became_moderator IS NOT NULL; + +CREATE INDEX idx_person_actions_person ON person_actions (person_id); + +CREATE INDEX idx_person_actions_target ON person_actions (target_id); + +CREATE INDEX idx_post_actions_person ON post_actions (person_id); + +CREATE INDEX idx_post_actions_post ON post_actions (post_id); + +-- Create new indexes, with `OR` being used to allow `IS NOT NULL` filters in queries to use either column in +-- a group (e.g. `liked IS NOT NULL` and `like_score IS NOT NULL` both work) +CREATE INDEX idx_comment_actions_liked_not_null ON comment_actions (person_id, comment_id) +WHERE + liked IS NOT NULL OR like_score IS NOT NULL; + +CREATE INDEX idx_comment_actions_saved_not_null ON comment_actions (person_id, comment_id) +WHERE + saved IS NOT NULL; + +CREATE INDEX idx_community_actions_followed_not_null ON community_actions (person_id, community_id) +WHERE + followed IS NOT NULL OR follow_state IS NOT NULL; + +CREATE INDEX idx_community_actions_blocked_not_null ON community_actions (person_id, community_id) +WHERE + blocked IS NOT NULL; + +CREATE INDEX idx_community_actions_became_moderator_not_null ON community_actions (person_id, community_id) +WHERE + became_moderator IS NOT NULL; + +CREATE INDEX idx_community_actions_received_ban_not_null ON community_actions (person_id, community_id) +WHERE + received_ban IS NOT NULL; + +CREATE INDEX idx_person_actions_followed_not_null ON person_actions (person_id, target_id) +WHERE + followed IS NOT NULL OR follow_pending IS NOT NULL; + +CREATE INDEX idx_person_actions_blocked_not_null ON person_actions (person_id, target_id) +WHERE + blocked IS NOT NULL; + +CREATE INDEX idx_post_actions_read_not_null ON post_actions (person_id, post_id) +WHERE + read IS NOT NULL; + +CREATE INDEX idx_post_actions_read_comments_not_null ON post_actions (person_id, post_id) +WHERE + read_comments IS NOT NULL OR read_comments_amount IS NOT NULL; + +CREATE INDEX idx_post_actions_saved_not_null ON post_actions (person_id, post_id) +WHERE + saved IS NOT NULL; + +CREATE INDEX idx_post_actions_liked_not_null ON post_actions (person_id, post_id) +WHERE + liked IS NOT NULL OR like_score IS NOT NULL; + +CREATE INDEX idx_post_actions_hidden_not_null ON post_actions (person_id, post_id) +WHERE + hidden IS NOT NULL; + +-- This index is currently redundant because instance_actions only has 1 action type, but inconsistency +-- with other tables would make it harder to do everything correctly when adding another action type +CREATE INDEX idx_instance_actions_blocked_not_null ON instance_actions (person_id, instance_id) +WHERE + blocked IS NOT NULL; + +-- Create new statistics for more accurate estimations of how much of an index will be read (e.g. for +-- `(liked, like_score)`, the query planner might othewise assume that `(TRUE, FALSE)` and `(TRUE, TRUE)` +-- are equally likely when only `(TRUE, TRUE)` is possible, which would make it severely underestimate +-- the efficiency of using the index) +CREATE statistics comment_actions_liked_stat ON (liked IS NULL), (like_score IS NULL) +FROM comment_actions; + +CREATE statistics community_actions_followed_stat ON (followed IS NULL), (follow_state IS NULL) +FROM community_actions; + +CREATE statistics person_actions_followed_stat ON (followed IS NULL), (follow_pending IS NULL) +FROM person_actions; + +CREATE statistics post_actions_read_comments_stat ON (read_comments IS NULL), (read_comments_amount IS NULL) +FROM post_actions; + +CREATE statistics post_actions_liked_stat ON (liked IS NULL), (like_score IS NULL), (post_id IS NULL) +FROM post_actions; + diff --git a/migrations/2024-11-12-090437_move-triggers/down.sql b/migrations/2024-11-12-090437_move-triggers/down.sql new file mode 100644 index 000000000..3607679bc --- /dev/null +++ b/migrations/2024-11-12-090437_move-triggers/down.sql @@ -0,0 +1,115 @@ +-- Edit community aggregates to include voters as active users +CREATE OR REPLACE FUNCTION community_aggregates_activity (i text) + RETURNS TABLE ( + count_ bigint, + community_id_ integer) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN query + SELECT + count(*), + community_id + FROM ( + SELECT + c.creator_id, + p.community_id + FROM + comment c + INNER JOIN post p ON c.post_id = p.id + INNER JOIN person pe ON c.creator_id = pe.id + WHERE + c.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + p.creator_id, + p.community_id + FROM + post p + INNER JOIN person pe ON p.creator_id = pe.id + WHERE + p.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + pl.person_id, + p.community_id + FROM + post_like pl + INNER JOIN post p ON pl.post_id = p.id + INNER JOIN person pe ON pl.person_id = pe.id + WHERE + pl.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE + UNION + SELECT + cl.person_id, + p.community_id + FROM + comment_like cl + INNER JOIN comment c ON cl.comment_id = comment.id + INNER JOIN post p ON comment.post_id = p.id + INNER JOIN person pe ON cl.person_id = pe.id + WHERE + cl.published > ('now'::timestamp - i::interval) + AND pe.bot_account = FALSE) a +GROUP BY + community_id; +END; +$$; + +-- Edit site aggregates to include voters and people who have read posts as active users +CREATE OR REPLACE FUNCTION site_aggregates_activity (i text) + RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE + count_ integer; +BEGIN + SELECT + count(*) INTO count_ + FROM ( + SELECT + c.creator_id + FROM + comment c + INNER JOIN person pe ON c.creator_id = pe.id + WHERE + c.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + p.creator_id + FROM + post p + INNER JOIN person pe ON p.creator_id = pe.id + WHERE + p.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + pl.person_id + FROM + post_like pl + INNER JOIN person pe ON pl.person_id = pe.id + WHERE + pl.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE + UNION + SELECT + cl.person_id + FROM + comment_like cl + INNER JOIN person pe ON cl.person_id = pe.id + WHERE + cl.published > ('now'::timestamp - i::interval) + AND pe.local = TRUE + AND pe.bot_account = FALSE) a; + RETURN count_; +END; +$$; + diff --git a/migrations/2024-11-12-090437_move-triggers/up.sql b/migrations/2024-11-12-090437_move-triggers/up.sql new file mode 100644 index 000000000..e7b2bd49d --- /dev/null +++ b/migrations/2024-11-12-090437_move-triggers/up.sql @@ -0,0 +1,2 @@ +DROP FUNCTION community_aggregates_activity, site_aggregates_activity CASCADE; + diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 37f2ff809..043d78d6b 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -22,7 +22,7 @@ use lemmy_db_schema::{ captcha_answer, comment, community, - community_person_ban, + community_actions, instance, person, post, @@ -36,7 +36,15 @@ use lemmy_db_schema::{ post::{Post, PostUpdateForm}, }, traits::Crud, - utils::{functions::coalesce, get_conn, naive_now, now, DbPool, DELETED_REPLACEMENT_TEXT}, + utils::{ + find_action, + functions::coalesce, + get_conn, + naive_now, + now, + DbPool, + DELETED_REPLACEMENT_TEXT, + }, }; use lemmy_routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use lemmy_utils::error::LemmyResult; @@ -395,7 +403,7 @@ async fn active_counts(pool: &mut DbPool<'_>) { for (full_form, abbr) in &intervals { let update_site_stmt = format!( - "update site_aggregates set users_active_{} = (select * from site_aggregates_activity('{}')) where site_id = 1", + "update site_aggregates set users_active_{} = (select * from r.site_aggregates_activity('{}')) where site_id = 1", abbr, full_form ); sql_query(update_site_stmt) @@ -404,7 +412,7 @@ async fn active_counts(pool: &mut DbPool<'_>) { .inspect_err(|e| error!("Failed to update site stats: {e}")) .ok(); - let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", abbr, full_form); + let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from r.community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", abbr, full_form); sql_query(update_community_stmt) .execute(&mut conn) .await @@ -439,7 +447,7 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { .ok(); diesel::delete( - community_person_ban::table.filter(community_person_ban::expires.lt(now().nullable())), + community_actions::table.filter(community_actions::ban_expires.lt(now().nullable())), ) .execute(&mut conn) .await @@ -470,11 +478,10 @@ async fn publish_scheduled_posts(context: &Data) { .filter(not(person::banned.or(person::deleted))) .filter(not(community::removed.or(community::deleted))) // ensure that user isnt banned from community - .filter(not(exists( - community_person_ban::table - .filter(community_person_ban::community_id.eq(community::id)) - .filter(community_person_ban::person_id.eq(person::id)), - ))) + .filter(not(exists(find_action( + community_actions::received_ban, + (person::id, community::id), + )))) .select((post::all_columns, community::all_columns)) .get_results::<(Post, Community)>(&mut conn) .await