]> Untitled Git - lemmy.git/blob - crates/db_views/src/post_view.rs
Fixing unstable post sorts. Fixes #2188 (#2204)
[lemmy.git] / crates / db_views / src / post_view.rs
1 use diesel::{dsl::*, pg::Pg, result::Error, *};
2 use lemmy_db_schema::{
3   aggregates::post_aggregates::PostAggregates,
4   functions::hot_rank,
5   fuzzy_search,
6   limit_and_offset,
7   newtypes::{CommunityId, DbUrl, PersonId, PostId},
8   schema::{
9     community,
10     community_block,
11     community_follower,
12     community_person_ban,
13     person,
14     person_block,
15     post,
16     post_aggregates,
17     post_like,
18     post_read,
19     post_saved,
20   },
21   source::{
22     community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
23     person::{Person, PersonSafe},
24     person_block::PersonBlock,
25     post::{Post, PostRead, PostSaved},
26   },
27   traits::{MaybeOptional, ToSafe, ViewToVec},
28   ListingType,
29   SortType,
30 };
31 use serde::{Deserialize, Serialize};
32 use tracing::debug;
33
34 #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
35 pub struct PostView {
36   pub post: Post,
37   pub creator: PersonSafe,
38   pub community: CommunitySafe,
39   pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan
40   pub counts: PostAggregates,
41   pub subscribed: bool,      // Left join to CommunityFollower
42   pub saved: bool,           // Left join to PostSaved
43   pub read: bool,            // Left join to PostRead
44   pub creator_blocked: bool, // Left join to PersonBlock
45   pub my_vote: Option<i16>,  // Left join to PostLike
46 }
47
48 type PostViewTuple = (
49   Post,
50   PersonSafe,
51   CommunitySafe,
52   Option<CommunityPersonBan>,
53   PostAggregates,
54   Option<CommunityFollower>,
55   Option<PostSaved>,
56   Option<PostRead>,
57   Option<PersonBlock>,
58   Option<i16>,
59 );
60
61 impl PostView {
62   pub fn read(
63     conn: &PgConnection,
64     post_id: PostId,
65     my_person_id: Option<PersonId>,
66   ) -> Result<Self, Error> {
67     // The left join below will return None in this case
68     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
69
70     let (
71       post,
72       creator,
73       community,
74       creator_banned_from_community,
75       counts,
76       follower,
77       saved,
78       read,
79       creator_blocked,
80       post_like,
81     ) = post::table
82       .find(post_id)
83       .inner_join(person::table)
84       .inner_join(community::table)
85       .left_join(
86         community_person_ban::table.on(
87           post::community_id
88             .eq(community_person_ban::community_id)
89             .and(community_person_ban::person_id.eq(post::creator_id))
90             .and(
91               community_person_ban::expires
92                 .is_null()
93                 .or(community_person_ban::expires.gt(now)),
94             ),
95         ),
96       )
97       .inner_join(post_aggregates::table)
98       .left_join(
99         community_follower::table.on(
100           post::community_id
101             .eq(community_follower::community_id)
102             .and(community_follower::person_id.eq(person_id_join)),
103         ),
104       )
105       .left_join(
106         post_saved::table.on(
107           post::id
108             .eq(post_saved::post_id)
109             .and(post_saved::person_id.eq(person_id_join)),
110         ),
111       )
112       .left_join(
113         post_read::table.on(
114           post::id
115             .eq(post_read::post_id)
116             .and(post_read::person_id.eq(person_id_join)),
117         ),
118       )
119       .left_join(
120         person_block::table.on(
121           post::creator_id
122             .eq(person_block::target_id)
123             .and(person_block::person_id.eq(person_id_join)),
124         ),
125       )
126       .left_join(
127         post_like::table.on(
128           post::id
129             .eq(post_like::post_id)
130             .and(post_like::person_id.eq(person_id_join)),
131         ),
132       )
133       .select((
134         post::all_columns,
135         Person::safe_columns_tuple(),
136         Community::safe_columns_tuple(),
137         community_person_ban::all_columns.nullable(),
138         post_aggregates::all_columns,
139         community_follower::all_columns.nullable(),
140         post_saved::all_columns.nullable(),
141         post_read::all_columns.nullable(),
142         person_block::all_columns.nullable(),
143         post_like::score.nullable(),
144       ))
145       .first::<PostViewTuple>(conn)?;
146
147     // If a person is given, then my_vote, if None, should be 0, not null
148     // Necessary to differentiate between other person's votes
149     let my_vote = if my_person_id.is_some() && post_like.is_none() {
150       Some(0)
151     } else {
152       post_like
153     };
154
155     Ok(PostView {
156       post,
157       creator,
158       community,
159       creator_banned_from_community: creator_banned_from_community.is_some(),
160       counts,
161       subscribed: follower.is_some(),
162       saved: saved.is_some(),
163       read: read.is_some(),
164       creator_blocked: creator_blocked.is_some(),
165       my_vote,
166     })
167   }
168 }
169
170 pub struct PostQueryBuilder<'a> {
171   conn: &'a PgConnection,
172   listing_type: Option<ListingType>,
173   sort: Option<SortType>,
174   creator_id: Option<PersonId>,
175   community_id: Option<CommunityId>,
176   community_actor_id: Option<DbUrl>,
177   my_person_id: Option<PersonId>,
178   search_term: Option<String>,
179   url_search: Option<String>,
180   show_nsfw: Option<bool>,
181   show_bot_accounts: Option<bool>,
182   show_read_posts: Option<bool>,
183   saved_only: Option<bool>,
184   page: Option<i64>,
185   limit: Option<i64>,
186 }
187
188 impl<'a> PostQueryBuilder<'a> {
189   pub fn create(conn: &'a PgConnection) -> Self {
190     PostQueryBuilder {
191       conn,
192       listing_type: None,
193       sort: None,
194       creator_id: None,
195       community_id: None,
196       community_actor_id: None,
197       my_person_id: None,
198       search_term: None,
199       url_search: None,
200       show_nsfw: None,
201       show_bot_accounts: None,
202       show_read_posts: None,
203       saved_only: None,
204       page: None,
205       limit: None,
206     }
207   }
208
209   pub fn listing_type<T: MaybeOptional<ListingType>>(mut self, listing_type: T) -> Self {
210     self.listing_type = listing_type.get_optional();
211     self
212   }
213
214   pub fn sort<T: MaybeOptional<SortType>>(mut self, sort: T) -> Self {
215     self.sort = sort.get_optional();
216     self
217   }
218
219   pub fn community_id<T: MaybeOptional<CommunityId>>(mut self, community_id: T) -> Self {
220     self.community_id = community_id.get_optional();
221     self
222   }
223
224   pub fn my_person_id<T: MaybeOptional<PersonId>>(mut self, my_person_id: T) -> Self {
225     self.my_person_id = my_person_id.get_optional();
226     self
227   }
228
229   pub fn community_actor_id<T: MaybeOptional<DbUrl>>(mut self, community_actor_id: T) -> Self {
230     self.community_actor_id = community_actor_id.get_optional();
231     self
232   }
233
234   pub fn creator_id<T: MaybeOptional<PersonId>>(mut self, creator_id: T) -> Self {
235     self.creator_id = creator_id.get_optional();
236     self
237   }
238
239   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
240     self.search_term = search_term.get_optional();
241     self
242   }
243
244   pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
245     self.url_search = url_search.get_optional();
246     self
247   }
248
249   pub fn show_nsfw<T: MaybeOptional<bool>>(mut self, show_nsfw: T) -> Self {
250     self.show_nsfw = show_nsfw.get_optional();
251     self
252   }
253
254   pub fn show_bot_accounts<T: MaybeOptional<bool>>(mut self, show_bot_accounts: T) -> Self {
255     self.show_bot_accounts = show_bot_accounts.get_optional();
256     self
257   }
258
259   pub fn show_read_posts<T: MaybeOptional<bool>>(mut self, show_read_posts: T) -> Self {
260     self.show_read_posts = show_read_posts.get_optional();
261     self
262   }
263
264   pub fn saved_only<T: MaybeOptional<bool>>(mut self, saved_only: T) -> Self {
265     self.saved_only = saved_only.get_optional();
266     self
267   }
268
269   pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
270     self.page = page.get_optional();
271     self
272   }
273
274   pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
275     self.limit = limit.get_optional();
276     self
277   }
278
279   pub fn list(self) -> Result<Vec<PostView>, Error> {
280     use diesel::dsl::*;
281
282     // The left join below will return None in this case
283     let person_id_join = self.my_person_id.unwrap_or(PersonId(-1));
284
285     let mut query = post::table
286       .inner_join(person::table)
287       .inner_join(community::table)
288       .left_join(
289         community_person_ban::table.on(
290           post::community_id
291             .eq(community_person_ban::community_id)
292             .and(community_person_ban::person_id.eq(post::creator_id))
293             .and(
294               community_person_ban::expires
295                 .is_null()
296                 .or(community_person_ban::expires.gt(now)),
297             ),
298         ),
299       )
300       .inner_join(post_aggregates::table)
301       .left_join(
302         community_follower::table.on(
303           post::community_id
304             .eq(community_follower::community_id)
305             .and(community_follower::person_id.eq(person_id_join)),
306         ),
307       )
308       .left_join(
309         post_saved::table.on(
310           post::id
311             .eq(post_saved::post_id)
312             .and(post_saved::person_id.eq(person_id_join)),
313         ),
314       )
315       .left_join(
316         post_read::table.on(
317           post::id
318             .eq(post_read::post_id)
319             .and(post_read::person_id.eq(person_id_join)),
320         ),
321       )
322       .left_join(
323         person_block::table.on(
324           post::creator_id
325             .eq(person_block::target_id)
326             .and(person_block::person_id.eq(person_id_join)),
327         ),
328       )
329       .left_join(
330         community_block::table.on(
331           community::id
332             .eq(community_block::community_id)
333             .and(community_block::person_id.eq(person_id_join)),
334         ),
335       )
336       .left_join(
337         post_like::table.on(
338           post::id
339             .eq(post_like::post_id)
340             .and(post_like::person_id.eq(person_id_join)),
341         ),
342       )
343       .select((
344         post::all_columns,
345         Person::safe_columns_tuple(),
346         Community::safe_columns_tuple(),
347         community_person_ban::all_columns.nullable(),
348         post_aggregates::all_columns,
349         community_follower::all_columns.nullable(),
350         post_saved::all_columns.nullable(),
351         post_read::all_columns.nullable(),
352         person_block::all_columns.nullable(),
353         post_like::score.nullable(),
354       ))
355       .into_boxed();
356
357     if let Some(listing_type) = self.listing_type {
358       match listing_type {
359         ListingType::Subscribed => {
360           query = query.filter(community_follower::person_id.is_not_null())
361         }
362         ListingType::Local => {
363           query = query.filter(community::local.eq(true)).filter(
364             community::hidden
365               .eq(false)
366               .or(community_follower::person_id.eq(person_id_join)),
367           );
368         }
369         ListingType::All => {
370           query = query.filter(
371             community::hidden
372               .eq(false)
373               .or(community_follower::person_id.eq(person_id_join)),
374           )
375         }
376         ListingType::Community => {
377           if let Some(community_id) = self.community_id {
378             query = query
379               .filter(post::community_id.eq(community_id))
380               .then_order_by(post_aggregates::stickied.desc());
381           }
382         }
383       }
384     }
385
386     if let Some(community_actor_id) = self.community_actor_id {
387       query = query
388         .filter(community::actor_id.eq(community_actor_id))
389         .then_order_by(post_aggregates::stickied.desc());
390     }
391
392     if let Some(url_search) = self.url_search {
393       query = query.filter(post::url.eq(url_search));
394     }
395
396     if let Some(search_term) = self.search_term {
397       let searcher = fuzzy_search(&search_term);
398       query = query.filter(
399         post::name
400           .ilike(searcher.to_owned())
401           .or(post::body.ilike(searcher)),
402       );
403     }
404
405     // If its for a specific person, show the removed / deleted
406     if let Some(creator_id) = self.creator_id {
407       query = query.filter(post::creator_id.eq(creator_id));
408     }
409
410     if !self.show_nsfw.unwrap_or(false) {
411       query = query
412         .filter(post::nsfw.eq(false))
413         .filter(community::nsfw.eq(false));
414     };
415
416     if !self.show_bot_accounts.unwrap_or(true) {
417       query = query.filter(person::bot_account.eq(false));
418     };
419
420     if self.saved_only.unwrap_or(false) {
421       query = query.filter(post_saved::id.is_not_null());
422     }
423     // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read
424     // setting wont be able to see saved posts.
425     else if !self.show_read_posts.unwrap_or(true) {
426       query = query.filter(post_read::id.is_null());
427     }
428
429     // Don't show blocked communities or persons
430     if self.my_person_id.is_some() {
431       query = query.filter(community_block::person_id.is_null());
432       query = query.filter(person_block::person_id.is_null());
433     }
434
435     query = match self.sort.unwrap_or(SortType::Hot) {
436       SortType::Active => query
437         .then_order_by(
438           hot_rank(
439             post_aggregates::score,
440             post_aggregates::newest_comment_time_necro,
441           )
442           .desc(),
443         )
444         .then_order_by(post_aggregates::newest_comment_time_necro.desc()),
445       SortType::Hot => query
446         .then_order_by(hot_rank(post_aggregates::score, post_aggregates::published).desc())
447         .then_order_by(post_aggregates::published.desc()),
448       SortType::New => query.then_order_by(post_aggregates::published.desc()),
449       SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
450       SortType::MostComments => query
451         .then_order_by(post_aggregates::comments.desc())
452         .then_order_by(post_aggregates::published.desc()),
453       SortType::TopAll => query
454         .then_order_by(post_aggregates::score.desc())
455         .then_order_by(post_aggregates::published.desc()),
456       SortType::TopYear => query
457         .filter(post_aggregates::published.gt(now - 1.years()))
458         .then_order_by(post_aggregates::score.desc())
459         .then_order_by(post_aggregates::published.desc()),
460       SortType::TopMonth => query
461         .filter(post_aggregates::published.gt(now - 1.months()))
462         .then_order_by(post_aggregates::score.desc())
463         .then_order_by(post_aggregates::published.desc()),
464       SortType::TopWeek => query
465         .filter(post_aggregates::published.gt(now - 1.weeks()))
466         .then_order_by(post_aggregates::score.desc())
467         .then_order_by(post_aggregates::published.desc()),
468       SortType::TopDay => query
469         .filter(post_aggregates::published.gt(now - 1.days()))
470         .then_order_by(post_aggregates::score.desc())
471         .then_order_by(post_aggregates::published.desc()),
472     };
473
474     let (limit, offset) = limit_and_offset(self.page, self.limit);
475
476     query = query
477       .limit(limit)
478       .offset(offset)
479       .filter(post::removed.eq(false))
480       .filter(post::deleted.eq(false))
481       .filter(community::removed.eq(false))
482       .filter(community::deleted.eq(false));
483
484     debug!("Post View Query: {:?}", debug_query::<Pg, _>(&query));
485
486     let res = query.load::<PostViewTuple>(self.conn)?;
487
488     Ok(PostView::from_tuple_to_vec(res))
489   }
490 }
491
492 impl ViewToVec for PostView {
493   type DbTuple = PostViewTuple;
494   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
495     items
496       .iter()
497       .map(|a| Self {
498         post: a.0.to_owned(),
499         creator: a.1.to_owned(),
500         community: a.2.to_owned(),
501         creator_banned_from_community: a.3.is_some(),
502         counts: a.4.to_owned(),
503         subscribed: a.5.is_some(),
504         saved: a.6.is_some(),
505         read: a.7.is_some(),
506         creator_blocked: a.8.is_some(),
507         my_vote: a.9,
508       })
509       .collect::<Vec<Self>>()
510   }
511 }
512
513 #[cfg(test)]
514 mod tests {
515   use crate::post_view::{PostQueryBuilder, PostView};
516   use lemmy_db_schema::{
517     aggregates::post_aggregates::PostAggregates,
518     establish_unpooled_connection,
519     source::{
520       community::*,
521       community_block::{CommunityBlock, CommunityBlockForm},
522       person::*,
523       person_block::{PersonBlock, PersonBlockForm},
524       post::*,
525     },
526     traits::{Blockable, Crud, Likeable},
527     ListingType,
528     SortType,
529   };
530   use serial_test::serial;
531
532   #[test]
533   #[serial]
534   fn test_crud() {
535     let conn = establish_unpooled_connection();
536
537     let person_name = "tegan".to_string();
538     let community_name = "test_community_3".to_string();
539     let post_name = "test post 3".to_string();
540     let bot_post_name = "test bot post".to_string();
541
542     let new_person = PersonForm {
543       name: person_name.to_owned(),
544       ..PersonForm::default()
545     };
546
547     let inserted_person = Person::create(&conn, &new_person).unwrap();
548
549     let new_bot = PersonForm {
550       name: person_name.to_owned(),
551       bot_account: Some(true),
552       ..PersonForm::default()
553     };
554
555     let inserted_bot = Person::create(&conn, &new_bot).unwrap();
556
557     let new_community = CommunityForm {
558       name: community_name.to_owned(),
559       title: "nada".to_owned(),
560       ..CommunityForm::default()
561     };
562
563     let inserted_community = Community::create(&conn, &new_community).unwrap();
564
565     // Test a person block, make sure the post query doesn't include their post
566     let blocked_person = PersonForm {
567       name: person_name.to_owned(),
568       ..PersonForm::default()
569     };
570
571     let inserted_blocked_person = Person::create(&conn, &blocked_person).unwrap();
572
573     let post_from_blocked_person = PostForm {
574       name: "blocked_person_post".to_string(),
575       creator_id: inserted_blocked_person.id,
576       community_id: inserted_community.id,
577       ..PostForm::default()
578     };
579
580     Post::create(&conn, &post_from_blocked_person).unwrap();
581
582     // block that person
583     let person_block = PersonBlockForm {
584       person_id: inserted_person.id,
585       target_id: inserted_blocked_person.id,
586     };
587
588     PersonBlock::block(&conn, &person_block).unwrap();
589
590     // A sample post
591     let new_post = PostForm {
592       name: post_name.to_owned(),
593       creator_id: inserted_person.id,
594       community_id: inserted_community.id,
595       ..PostForm::default()
596     };
597
598     let inserted_post = Post::create(&conn, &new_post).unwrap();
599
600     let new_bot_post = PostForm {
601       name: bot_post_name,
602       creator_id: inserted_bot.id,
603       community_id: inserted_community.id,
604       ..PostForm::default()
605     };
606
607     let _inserted_bot_post = Post::create(&conn, &new_bot_post).unwrap();
608
609     let post_like_form = PostLikeForm {
610       post_id: inserted_post.id,
611       person_id: inserted_person.id,
612       score: 1,
613     };
614
615     let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap();
616
617     let expected_post_like = PostLike {
618       id: inserted_post_like.id,
619       post_id: inserted_post.id,
620       person_id: inserted_person.id,
621       published: inserted_post_like.published,
622       score: 1,
623     };
624
625     let read_post_listings_with_person = PostQueryBuilder::create(&conn)
626       .listing_type(ListingType::Community)
627       .sort(SortType::New)
628       .show_bot_accounts(false)
629       .community_id(inserted_community.id)
630       .my_person_id(inserted_person.id)
631       .list()
632       .unwrap();
633
634     let read_post_listings_no_person = PostQueryBuilder::create(&conn)
635       .listing_type(ListingType::Community)
636       .sort(SortType::New)
637       .community_id(inserted_community.id)
638       .list()
639       .unwrap();
640
641     let read_post_listing_no_person = PostView::read(&conn, inserted_post.id, None).unwrap();
642     let read_post_listing_with_person =
643       PostView::read(&conn, inserted_post.id, Some(inserted_person.id)).unwrap();
644
645     let agg = PostAggregates::read(&conn, inserted_post.id).unwrap();
646
647     // the non person version
648     let expected_post_listing_no_person = PostView {
649       post: Post {
650         id: inserted_post.id,
651         name: post_name,
652         creator_id: inserted_person.id,
653         url: None,
654         body: None,
655         published: inserted_post.published,
656         updated: None,
657         community_id: inserted_community.id,
658         removed: false,
659         deleted: false,
660         locked: false,
661         stickied: false,
662         nsfw: false,
663         embed_title: None,
664         embed_description: None,
665         embed_html: None,
666         thumbnail_url: None,
667         ap_id: inserted_post.ap_id.to_owned(),
668         local: true,
669       },
670       my_vote: None,
671       creator: PersonSafe {
672         id: inserted_person.id,
673         name: person_name,
674         display_name: None,
675         published: inserted_person.published,
676         avatar: None,
677         actor_id: inserted_person.actor_id.to_owned(),
678         local: true,
679         admin: false,
680         bot_account: false,
681         banned: false,
682         deleted: false,
683         bio: None,
684         banner: None,
685         updated: None,
686         inbox_url: inserted_person.inbox_url.to_owned(),
687         shared_inbox_url: None,
688         matrix_user_id: None,
689         ban_expires: None,
690       },
691       creator_banned_from_community: false,
692       community: CommunitySafe {
693         id: inserted_community.id,
694         name: community_name,
695         icon: None,
696         removed: false,
697         deleted: false,
698         nsfw: false,
699         actor_id: inserted_community.actor_id.to_owned(),
700         local: true,
701         title: "nada".to_owned(),
702         description: None,
703         updated: None,
704         banner: None,
705         hidden: false,
706         published: inserted_community.published,
707       },
708       counts: PostAggregates {
709         id: agg.id,
710         post_id: inserted_post.id,
711         comments: 0,
712         score: 1,
713         upvotes: 1,
714         downvotes: 0,
715         stickied: false,
716         published: agg.published,
717         newest_comment_time_necro: inserted_post.published,
718         newest_comment_time: inserted_post.published,
719       },
720       subscribed: false,
721       read: false,
722       saved: false,
723       creator_blocked: false,
724     };
725
726     // Test a community block
727     let community_block = CommunityBlockForm {
728       person_id: inserted_person.id,
729       community_id: inserted_community.id,
730     };
731     CommunityBlock::block(&conn, &community_block).unwrap();
732
733     let read_post_listings_with_person_after_block = PostQueryBuilder::create(&conn)
734       .listing_type(ListingType::Community)
735       .sort(SortType::New)
736       .show_bot_accounts(false)
737       .community_id(inserted_community.id)
738       .my_person_id(inserted_person.id)
739       .list()
740       .unwrap();
741
742     // TODO More needs to be added here
743     let mut expected_post_listing_with_user = expected_post_listing_no_person.to_owned();
744     expected_post_listing_with_user.my_vote = Some(1);
745
746     let like_removed = PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap();
747     let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
748     PersonBlock::unblock(&conn, &person_block).unwrap();
749     CommunityBlock::unblock(&conn, &community_block).unwrap();
750     Community::delete(&conn, inserted_community.id).unwrap();
751     Person::delete(&conn, inserted_person.id).unwrap();
752     Person::delete(&conn, inserted_bot.id).unwrap();
753     Person::delete(&conn, inserted_blocked_person.id).unwrap();
754
755     // The with user
756     assert_eq!(
757       expected_post_listing_with_user,
758       read_post_listings_with_person[0]
759     );
760     assert_eq!(
761       expected_post_listing_with_user,
762       read_post_listing_with_person
763     );
764
765     // Should be only one person, IE the bot post, and blocked should be missing
766     assert_eq!(1, read_post_listings_with_person.len());
767
768     // Without the user
769     assert_eq!(
770       expected_post_listing_no_person,
771       read_post_listings_no_person[1]
772     );
773     assert_eq!(expected_post_listing_no_person, read_post_listing_no_person);
774
775     // Should be 2 posts, with the bot post, and the blocked
776     assert_eq!(3, read_post_listings_no_person.len());
777
778     // Should be 0 posts after the community block
779     assert_eq!(0, read_post_listings_with_person_after_block.len());
780
781     assert_eq!(expected_post_like, inserted_post_like);
782     assert_eq!(1, like_removed);
783     assert_eq!(1, num_deleted);
784   }
785 }