diff --git a/i18n/en.json b/i18n/en.json index 9bb5801a0d..62e2556fc9 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6618,6 +6618,10 @@ "id": "store.sql_post.permanent_delete_by_user.too_many.app_error", "translation": "Unable to select the posts to delete for the user (too many), please re-run." }, + { + "id": "store.sql_post.populate_reply_count.app_error", + "translation": "Unable to get the post replies count" + }, { "id": "store.sql_post.save.app_error", "translation": "Unable to save the Post." diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index e454584cc0..1551ca9830 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -172,12 +172,21 @@ func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, *model. } } + unknownRepliesPosts := []*model.Post{} for _, post := range posts { if len(post.RootId) == 0 { count, ok := rootIds[post.Id] if ok { post.ReplyCount += int64(count) } + } else { + unknownRepliesPosts = append(unknownRepliesPosts, post) + } + } + + if len(unknownRepliesPosts) > 0 { + if err := s.populateReplyCount(unknownRepliesPosts); err != nil { + mlog.Error("Unable to populate the reply count in some posts.", mlog.Err(err)) } } @@ -192,6 +201,42 @@ func (s *SqlPostStore) Save(post *model.Post) (*model.Post, *model.AppError) { return posts[0], nil } +func (s *SqlPostStore) populateReplyCount(posts []*model.Post) *model.AppError { + rootIds := []string{} + for _, post := range posts { + rootIds = append(rootIds, post.Id) + } + countList := []struct { + RootId string + Count int64 + }{} + query := s.getQueryBuilder().Select("RootId, COUNT(Id)").From("Posts").Where(sq.Eq{"RootId": rootIds}).Where(sq.Eq{"DeleteAt": 0}).GroupBy("RootId") + + queryString, args, err := query.ToSql() + if err != nil { + return model.NewAppError("SqlPostStore.populateReplyCount", "store.sql_post.populate_reply_count.app_error", nil, err.Error(), http.StatusInternalServerError) + } + _, err = s.GetMaster().Select(&countList, queryString, args...) + if err != nil { + return model.NewAppError("SqlPostStore.populateReplyCount", "store.sql_post.populate_reply_count.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + counts := map[string]int64{} + for _, count := range countList { + counts[count.RootId] = count.Count + } + + for _, post := range posts { + count, ok := counts[post.RootId] + if !ok { + post.ReplyCount = 0 + } + post.ReplyCount = count + } + + return nil +} + func (s *SqlPostStore) Update(newPost *model.Post, oldPost *model.Post) (*model.Post, *model.AppError) { newPost.UpdateAt = model.GetMillis() newPost.PreCommit() @@ -270,7 +315,7 @@ func (s *SqlPostStore) GetFlaggedPosts(userId string, offset int, limit int) (*m pl := model.NewPostList() var posts []*model.Post - if _, err := s.GetReplica().Select(&posts, "SELECT *, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = p.Id AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE Id IN (SELECT Name FROM Preferences WHERE UserId = :UserId AND Category = :Category) AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT :Limit OFFSET :Offset", map[string]interface{}{"UserId": userId, "Category": model.PREFERENCE_CATEGORY_FLAGGED_POST, "Offset": offset, "Limit": limit}); err != nil { + if _, err := s.GetReplica().Select(&posts, "SELECT *, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE Id IN (SELECT Name FROM Preferences WHERE UserId = :UserId AND Category = :Category) AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT :Limit OFFSET :Offset", map[string]interface{}{"UserId": userId, "Category": model.PREFERENCE_CATEGORY_FLAGGED_POST, "Offset": offset, "Limit": limit}); err != nil { return nil, model.NewAppError("SqlPostStore.GetFlaggedPosts", "store.sql_post.get_flagged_posts.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -289,7 +334,7 @@ func (s *SqlPostStore) GetFlaggedPostsForTeam(userId, teamId string, offset int, query := ` SELECT - A.*, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = A.Id AND Posts.DeleteAt = 0) as ReplyCount + A.*, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN A.RootId = '' THEN A.Id ELSE A.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM (SELECT * @@ -331,7 +376,7 @@ func (s *SqlPostStore) GetFlaggedPostsForChannel(userId, channelId string, offse var posts []*model.Post query := ` SELECT - *, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = p.Id AND Posts.DeleteAt = 0) as ReplyCount + *, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE Id IN (SELECT Name FROM Preferences WHERE UserId = :UserId AND Category = :Category) @@ -359,7 +404,7 @@ func (s *SqlPostStore) Get(id string, skipFetchThreads bool) (*model.PostList, * } var post model.Post - postFetchQuery := "SELECT p.*, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = p.Id AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE p.Id = :Id AND p.DeleteAt = 0" + postFetchQuery := "SELECT p.*, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE p.Id = :Id AND p.DeleteAt = 0" err := s.GetReplica().SelectOne(&post, postFetchQuery, map[string]interface{}{"Id": id}) if err != nil { return nil, model.NewAppError("SqlPostStore.GetPost", "store.sql_post.get.app_error", nil, "id="+id+err.Error(), http.StatusNotFound) @@ -378,7 +423,7 @@ func (s *SqlPostStore) Get(id string, skipFetchThreads bool) (*model.PostList, * } var posts []*model.Post - _, err = s.GetReplica().Select(&posts, "SELECT *, (SELECT count(Id) FROM Posts WHERE RootId = p.Id AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE (Id = :Id OR RootId = :RootId) AND DeleteAt = 0", map[string]interface{}{"Id": rootId, "RootId": rootId}) + _, err = s.GetReplica().Select(&posts, "SELECT *, (SELECT count(Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE (Id = :Id OR RootId = :RootId) AND DeleteAt = 0", map[string]interface{}{"Id": rootId, "RootId": rootId}) if err != nil { return nil, model.NewAppError("SqlPostStore.GetPost", "store.sql_post.get.app_error", nil, "root_id="+rootId+err.Error(), http.StatusInternalServerError) } @@ -557,8 +602,8 @@ func (s *SqlPostStore) GetPostsSince(options model.GetPostsSinceOptions, allowFr replyCountQuery1 := "" replyCountQuery2 := "" if options.SkipFetchThreads { - replyCountQuery1 = `, (SELECT COUNT(Posts.Id) FROM Posts WHERE p1.RootId = '' AND Posts.RootId = p1.Id AND Posts.DeleteAt = 0) as ReplyCount` - replyCountQuery2 = `, (SELECT COUNT(Posts.Id) FROM Posts WHERE p2.RootId = '' AND Posts.RootId = p2.Id AND Posts.DeleteAt = 0) as ReplyCount` + replyCountQuery1 = `, (SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p1.RootId = '' THEN p1.Id ELSE p1.RootId END) AND Posts.DeleteAt = 0) as ReplyCount` + replyCountQuery2 = `, (SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p2.RootId = '' THEN p2.Id ELSE p2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount` } var query string @@ -825,10 +870,11 @@ func (s *SqlPostStore) getRootPosts(channelId string, offset int, limit int, ski var posts []*model.Post var fetchQuery string if skipFetchThreads { - fetchQuery = "SELECT p.*, (SELECT COUNT(Posts.Id) FROM Posts WHERE p.RootId = '' AND Posts.RootId = p.Id AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE ChannelId = :ChannelId AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT :Limit OFFSET :Offset" + fetchQuery = "SELECT p.*, (SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE ChannelId = :ChannelId AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT :Limit OFFSET :Offset" } else { fetchQuery = "SELECT * FROM Posts WHERE ChannelId = :ChannelId AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT :Limit OFFSET :Offset" } + mlog.Debug(fetchQuery, mlog.Any("params", map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit})) _, err := s.GetReplica().Select(&posts, fetchQuery, map[string]interface{}{"ChannelId": channelId, "Offset": offset, "Limit": limit}) if err != nil { return nil, model.NewAppError("SqlPostStore.GetLinearPosts", "store.sql_post.get_root_posts.app_error", nil, "channelId="+channelId+err.Error(), http.StatusInternalServerError) @@ -879,7 +925,7 @@ func (s *SqlPostStore) getParentsPosts(channelId string, offset int, limit int, replyCountQuery := "" whereStatement := "p.Id IN (" + placeholderString + ")" if skipFetchThreads { - replyCountQuery = `, (SELECT COUNT(Posts.Id) FROM Posts WHERE p.RootId = '' AND Posts.RootId = p.Id AND Posts.DeleteAt = 0) as ReplyCount` + replyCountQuery = `, (SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount` } else { whereStatement += " OR p.RootId IN (" + placeholderString + ")" } @@ -905,7 +951,7 @@ func (s *SqlPostStore) getParentsPostsPostgreSQL(channelId string, offset int, l replyCountQuery := "" onStatement := "q1.RootId = q2.Id" if skipFetchThreads { - replyCountQuery = ` ,(SELECT COUNT(Posts.Id) FROM Posts WHERE q2.RootId = '' AND Posts.RootId = q2.Id AND Posts.DeleteAt = 0) as ReplyCount` + replyCountQuery = ` ,(SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount` } else { onStatement += " OR q1.RootId = q2.RootId" } @@ -1112,7 +1158,7 @@ func (s *SqlPostStore) search(teamId string, userId string, params *model.Search searchQuery := ` SELECT - * ,(SELECT COUNT(Posts.Id) FROM Posts WHERE q2.RootId = '' AND Posts.RootId = q2.Id AND Posts.DeleteAt = 0) as ReplyCount + * ,(SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts q2 WHERE @@ -1395,7 +1441,7 @@ func (s *SqlPostStore) GetPostsCreatedAt(channelId string, time int64) ([]*model func (s *SqlPostStore) GetPostsByIds(postIds []string) ([]*model.Post, *model.AppError) { keys, params := MapStringsToQueryParams(postIds, "Post") - query := `SELECT * FROM Posts WHERE Id IN ` + keys + ` ORDER BY CreateAt DESC` + query := `SELECT p.*, (SELECT count(Posts.Id) FROM Posts WHERE Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0) as ReplyCount FROM Posts p WHERE p.Id IN ` + keys + ` ORDER BY CreateAt DESC` var posts []*model.Post _, err := s.GetReplica().Select(&posts, query, params) diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index 6223e5c58c..1a4d15819c 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -33,6 +33,7 @@ func TestPostStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("GetPostsWithDetails", func(t *testing.T) { testPostStoreGetPostsWithDetails(t, ss) }) t.Run("GetPostsBeforeAfter", func(t *testing.T) { testPostStoreGetPostsBeforeAfter(t, ss) }) t.Run("GetPostsSince", func(t *testing.T) { testPostStoreGetPostsSince(t, ss) }) + t.Run("GetPosts", func(t *testing.T) { testPostStoreGetPosts(t, ss) }) t.Run("GetPostBeforeAfter", func(t *testing.T) { testPostStoreGetPostBeforeAfter(t, ss) }) t.Run("UserCountsWithPostsByDay", func(t *testing.T) { testUserCountsWithPostsByDay(t, ss) }) t.Run("PostCountsByDay", func(t *testing.T) { testPostCountsByDay(t, ss) }) @@ -1215,6 +1216,118 @@ func testPostStoreGetPostsSince(t *testing.T, ss store.Store) { }) } +func testPostStoreGetPosts(t *testing.T, ss store.Store) { + channelId := model.NewId() + userId := model.NewId() + + post1, err := ss.Post().Save(&model.Post{ + ChannelId: channelId, + UserId: userId, + Message: "message", + }) + require.Nil(t, err) + time.Sleep(time.Millisecond) + + post2, err := ss.Post().Save(&model.Post{ + ChannelId: channelId, + UserId: userId, + Message: "message", + }) + require.Nil(t, err) + time.Sleep(time.Millisecond) + + post3, err := ss.Post().Save(&model.Post{ + ChannelId: channelId, + UserId: userId, + Message: "message", + }) + require.Nil(t, err) + time.Sleep(time.Millisecond) + + post4, err := ss.Post().Save(&model.Post{ + ChannelId: channelId, + UserId: userId, + Message: "message", + }) + require.Nil(t, err) + time.Sleep(time.Millisecond) + + post5, err := ss.Post().Save(&model.Post{ + ChannelId: channelId, + UserId: userId, + Message: "message", + RootId: post3.Id, + }) + require.Nil(t, err) + time.Sleep(time.Millisecond) + + post6, err := ss.Post().Save(&model.Post{ + ChannelId: channelId, + UserId: userId, + Message: "message", + RootId: post1.Id, + }) + require.Nil(t, err) + + t.Run("should return the last posts created in a channel", func(t *testing.T) { + postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 30, SkipFetchThreads: false}, false) + assert.Nil(t, err) + + assert.Equal(t, []string{ + post6.Id, + post5.Id, + post4.Id, + post3.Id, + post2.Id, + post1.Id, + }, postList.Order) + + assert.Len(t, postList.Posts, 6) + assert.NotNil(t, postList.Posts[post1.Id]) + assert.NotNil(t, postList.Posts[post2.Id]) + assert.NotNil(t, postList.Posts[post3.Id]) + assert.NotNil(t, postList.Posts[post4.Id]) + assert.NotNil(t, postList.Posts[post5.Id]) + assert.NotNil(t, postList.Posts[post6.Id]) + }) + + t.Run("should return the last posts created in a channel and the threads and the reply count must be 0", func(t *testing.T) { + postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 2, SkipFetchThreads: false}, false) + assert.Nil(t, err) + + assert.Equal(t, []string{ + post6.Id, + post5.Id, + }, postList.Order) + + assert.Len(t, postList.Posts, 4) + require.NotNil(t, postList.Posts[post1.Id]) + require.NotNil(t, postList.Posts[post3.Id]) + require.NotNil(t, postList.Posts[post5.Id]) + require.NotNil(t, postList.Posts[post6.Id]) + assert.Equal(t, int64(0), postList.Posts[post1.Id].ReplyCount) + assert.Equal(t, int64(0), postList.Posts[post3.Id].ReplyCount) + assert.Equal(t, int64(0), postList.Posts[post5.Id].ReplyCount) + assert.Equal(t, int64(0), postList.Posts[post6.Id].ReplyCount) + }) + + t.Run("should return the last posts created in a channel without the threads and the reply count must be correct", func(t *testing.T) { + postList, err := ss.Post().GetPosts(model.GetPostsOptions{ChannelId: channelId, Page: 0, PerPage: 2, SkipFetchThreads: true}, false) + assert.Nil(t, err) + + assert.Equal(t, []string{ + post6.Id, + post5.Id, + }, postList.Order) + + assert.Len(t, postList.Posts, 4) + assert.NotNil(t, postList.Posts[post5.Id]) + assert.NotNil(t, postList.Posts[post6.Id]) + assert.Equal(t, int64(1), postList.Posts[post5.Id].ReplyCount) + assert.Equal(t, int64(1), postList.Posts[post6.Id].ReplyCount) + }) +} + func testPostStoreGetPostBeforeAfter(t *testing.T, ss store.Store) { channelId := model.NewId()