Этот коммит содержится в:
Jesse Hallam
2022-04-19 17:06:31 -03:00
коммит произвёл GitHub
родитель 348602cf00
Коммит 5bd223c836
12 изменённых файлов: 465 добавлений и 282 удалений

Просмотреть файл

@@ -2491,13 +2491,12 @@ func TestCollapsedThreadFetch(t *testing.T) {
}() }()
require.NotPanics(t, func() { require.NotPanics(t, func() {
_, err = th.App.CreatePost(th.Context, &model.Post{ th.App.CreatePost(th.Context, &model.Post{
UserId: user1.Id, UserId: user1.Id,
ChannelId: channel.Id, ChannelId: channel.Id,
RootId: postRoot.Id, RootId: postRoot.Id,
Message: fmt.Sprintf("@%s", user2.Username), Message: fmt.Sprintf("@%s", user2.Username),
}, channel, false, true) }, channel, false, true)
require.Nil(t, err)
}) })
wg.Wait() wg.Wait()

Просмотреть файл

@@ -2311,15 +2311,20 @@ func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThre
return nil return nil
}) })
eg.Go(func() error { // Unread is a legacy flag that caused GetTotalThreads to compute the same value as
totalCount, err := a.Srv().Store.Thread().GetTotalThreads(userID, teamID, options) // GetTotalUnreadThreads. If unspecified, do this work normally; otherwise, skip,
if err != nil { // and send back duplicate values down below.
return errors.Wrapf(err, "failed to count threads for user id=%s", userID) if !options.Unread {
} eg.Go(func() error {
result.Total = totalCount totalCount, err := a.Srv().Store.Thread().GetTotalThreads(userID, teamID, options)
if err != nil {
return errors.Wrapf(err, "failed to count threads for user id=%s", userID)
}
result.Total = totalCount
return nil return nil
}) })
}
eg.Go(func() error { eg.Go(func() error {
totalUnreadMentions, err := a.Srv().Store.Thread().GetTotalUnreadMentions(userID, teamID, options) totalUnreadMentions, err := a.Srv().Store.Thread().GetTotalUnreadMentions(userID, teamID, options)
@@ -2348,6 +2353,10 @@ func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThre
return nil, model.NewAppError("GetThreadsForUser", "app.user.get_threads_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) return nil, model.NewAppError("GetThreadsForUser", "app.user.get_threads_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
} }
if options.Unread {
result.Total = result.TotalUnreadThreads
}
for _, thread := range result.Threads { for _, thread := range result.Threads {
a.sanitizeProfiles(thread.Participants, false) a.sanitizeProfiles(thread.Participants, false)
thread.Post.SanitizeProps() thread.Post.SanitizeProps()

Просмотреть файл

@@ -0,0 +1,14 @@
SET @preparedStatement = (SELECT IF(
EXISTS(
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_name = 'Threads'
AND table_schema = DATABASE()
AND column_name = 'DeleteAt'
) > 0,
'ALTER TABLE Threads DROP COLUMN DeleteAt;',
'SELECT 1;'
));
PREPARE removeColumnIfExists FROM @preparedStatement;
EXECUTE removeColumnIfExists;
DEALLOCATE PREPARE removeColumnIfExists;

Просмотреть файл

@@ -0,0 +1,19 @@
SET @preparedStatement = (SELECT IF(
NOT EXISTS(
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Threads'
AND table_schema = DATABASE()
AND column_name = 'DeleteAt'
),
'ALTER TABLE Threads ADD COLUMN DeleteAt bigint(20);',
'SELECT 1;'
));
PREPARE addColumnIfNotExists FROM @preparedStatement;
EXECUTE addColumnIfNotExists;
DEALLOCATE PREPARE addColumnIfNotExists;
UPDATE Threads, Posts
SET Threads.DeleteAt = Posts.DeleteAt
WHERE Posts.Id = Threads.PostId
AND Threads.DeleteAt IS NULL;

Просмотреть файл

@@ -0,0 +1 @@
ALTER TABLE threads DROP COLUMN IF EXISTS deleteat;

Просмотреть файл

@@ -0,0 +1,2 @@
ALTER TABLE threads ADD COLUMN IF NOT EXISTS deleteat bigint;
UPDATE threads SET deleteat = posts.deleteat FROM posts WHERE threads.deleteat IS NULL AND posts.id = threads.postid;

Просмотреть файл

@@ -22,6 +22,9 @@ type Thread struct {
// Participants is a list of user ids that have replied to the thread, sorted by the oldest // Participants is a list of user ids that have replied to the thread, sorted by the oldest
// to newest. Note that the root post author is not included in this list until they reply. // to newest. Note that the root post author is not included in this list until they reply.
Participants StringArray `json:"participants"` Participants StringArray `json:"participants"`
// DeleteAt is a denormalized copy of the root posts's DeleteAt.
DeleteAt int64 `json:"delete_at"`
} }
type ThreadResponse struct { type ThreadResponse struct {
@@ -33,6 +36,7 @@ type ThreadResponse struct {
Post *Post `json:"post"` Post *Post `json:"post"`
UnreadReplies int64 `json:"unread_replies"` UnreadReplies int64 `json:"unread_replies"`
UnreadMentions int64 `json:"unread_mentions"` UnreadMentions int64 `json:"unread_mentions"`
DeleteAt int64 `json:"delete_at"`
} }
type Threads struct { type Threads struct {

Просмотреть файл

@@ -215,7 +215,7 @@ func (s *SqlPostStore) SaveMultiple(posts []*model.Post) ([]*model.Post, int, er
} }
if err = s.updateThreadsFromPosts(transaction, posts); err != nil { if err = s.updateThreadsFromPosts(transaction, posts); err != nil {
mlog.Warn("Error updating posts, thread update failed", mlog.Err(err)) return nil, -1, errors.Wrap(err, "update thread from posts failed")
} }
if err = transaction.Commit(); err != nil { if err = transaction.Commit(); err != nil {
@@ -290,7 +290,7 @@ func (s *SqlPostStore) populateReplyCount(posts []*model.Post) error {
Select("RootId, COUNT(Id) AS Count"). Select("RootId, COUNT(Id) AS Count").
From("Posts"). From("Posts").
Where(sq.Eq{"RootId": rootIds}). Where(sq.Eq{"RootId": rootIds}).
Where(sq.Eq{"DeleteAt": 0}). Where(sq.Eq{"Posts.DeleteAt": 0}).
GroupBy("RootId") GroupBy("RootId")
queryString, args, err := query.ToSql() queryString, args, err := query.ToSql()
@@ -487,7 +487,7 @@ func (s *SqlPostStore) getFlaggedPosts(userId, channelId, teamId string, offset
AND Category = ? AND Category = ?
) )
CHANNEL_FILTER CHANNEL_FILTER
AND DeleteAt = 0 AND Posts.DeleteAt = 0
) as A ) as A
INNER JOIN Channels as B INNER JOIN Channels as B
ON B.Id = A.ChannelId ON B.Id = A.ChannelId
@@ -569,8 +569,8 @@ func (s *SqlPostStore) getPostWithCollapsedThreads(id, userID string, opts model
From("Posts"). From("Posts").
LeftJoin("Threads ON Threads.PostId = Id"). LeftJoin("Threads ON Threads.PostId = Id").
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Id AND ThreadMemberships.UserId = ?", userID). LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Id AND ThreadMemberships.UserId = ?", userID).
Where(sq.Eq{"DeleteAt": 0}). Where(sq.Eq{"Posts.DeleteAt": 0}).
Where(sq.Eq{"Id": id}).ToSql() Where(sq.Eq{"Posts.Id": id}).ToSql()
err := s.GetReplicaX().Get(&post, postFetchQuery, args...) err := s.GetReplicaX().Get(&post, postFetchQuery, args...)
if err != nil { if err != nil {
@@ -586,8 +586,8 @@ func (s *SqlPostStore) getPostWithCollapsedThreads(id, userID string, opts model
Select("*"). Select("*").
From("Posts"). From("Posts").
Where(sq.Eq{ Where(sq.Eq{
"RootId": id, "Posts.RootId": id,
"DeleteAt": 0, "Posts.DeleteAt": 0,
}) })
var sort string var sort string
@@ -873,7 +873,11 @@ func (s *SqlPostStore) Delete(postID string, time int64, deleteByID string) erro
return errors.Wrap(err, "failed to update Posts") return errors.Wrap(err, "failed to update Posts")
} }
err = s.cleanupThreadComments(transaction, postID, id.RootId, id.UserId) if id.RootId == "" {
err = s.deleteThread(transaction, postID, time)
} else {
err = s.updateThreadAfterReplyDeletion(transaction, id.RootId, id.UserId)
}
if err != nil { if err != nil {
return errors.Wrapf(err, "failed to cleanup Thread with postid=%s", id.RootId) return errors.Wrapf(err, "failed to cleanup Thread with postid=%s", id.RootId)
@@ -933,13 +937,12 @@ func (s *SqlPostStore) permanentDeleteAllCommentByUser(userId string) error {
} }
_, err = transaction.Exec("DELETE FROM Posts WHERE UserId = ? AND RootId != ''", userId) _, err = transaction.Exec("DELETE FROM Posts WHERE UserId = ? AND RootId != ''", userId)
if err != nil { if err != nil {
return errors.Wrapf(err, "failed to delete Posts with userId=%s", userId) return errors.Wrapf(err, "failed to delete Posts with userId=%s", userId)
} }
for _, ids := range results { for _, ids := range results {
if err = s.cleanupThreadComments(transaction, ids.Id, ids.RootId, userId); err != nil { if err = s.updateThreadAfterReplyDeletion(transaction, ids.RootId, userId); err != nil {
return err return err
} }
} }
@@ -1103,14 +1106,14 @@ func (s *SqlPostStore) getPostsCollapsedThreads(options model.GetPostsOptions) (
postFetchQuery, args, _ := s.getQueryBuilder(). postFetchQuery, args, _ := s.getQueryBuilder().
Select(columns...). Select(columns...).
From("Posts"). From("Posts").
LeftJoin("Threads ON Threads.PostId = Id"). LeftJoin("Threads ON Threads.PostId = Posts.Id").
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Id AND ThreadMemberships.UserId = ?", options.UserId). LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Posts.Id AND ThreadMemberships.UserId = ?", options.UserId).
Where(sq.Eq{"DeleteAt": 0}). Where(sq.Eq{"Posts.DeleteAt": 0}).
Where(sq.Eq{"Posts.ChannelId": options.ChannelId}). Where(sq.Eq{"Posts.ChannelId": options.ChannelId}).
Where(sq.Eq{"RootId": ""}). Where(sq.Eq{"Posts.RootId": ""}).
Limit(uint64(options.PerPage)). Limit(uint64(options.PerPage)).
Offset(uint64(offset)). Offset(uint64(offset)).
OrderBy("CreateAt DESC").ToSql() OrderBy("Posts.CreateAt DESC").ToSql()
err := s.GetReplicaX().Select(&posts, postFetchQuery, args...) err := s.GetReplicaX().Select(&posts, postFetchQuery, args...)
if err != nil { if err != nil {
@@ -1187,13 +1190,13 @@ func (s *SqlPostStore) getPostsSinceCollapsedThreads(options model.GetPostsSince
postFetchQuery, args, _ := s.getQueryBuilder(). postFetchQuery, args, _ := s.getQueryBuilder().
Select(columns...). Select(columns...).
From("Posts"). From("Posts").
LeftJoin("Threads ON Threads.PostId = Id"). LeftJoin("Threads ON Threads.PostId = Posts.Id").
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Id AND ThreadMemberships.UserId = ?", options.UserId). LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Posts.Id AND ThreadMemberships.UserId = ?", options.UserId).
Where(sq.Eq{"DeleteAt": 0}). Where(sq.Eq{"Posts.DeleteAt": 0}).
Where(sq.Eq{"Posts.ChannelId": options.ChannelId}). Where(sq.Eq{"Posts.ChannelId": options.ChannelId}).
Where(sq.Gt{"UpdateAt": options.Time}). Where(sq.Gt{"Posts.UpdateAt": options.Time}).
Where(sq.Eq{"RootId": ""}). Where(sq.Eq{"Posts.RootId": ""}).
OrderBy("CreateAt DESC").ToSql() OrderBy("Posts.CreateAt DESC").ToSql()
err := s.GetReplicaX().Select(&posts, postFetchQuery, args...) err := s.GetReplicaX().Select(&posts, postFetchQuery, args...)
if err != nil { if err != nil {
@@ -1316,16 +1319,16 @@ func (s *SqlPostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOp
query := s.getQueryBuilder(). query := s.getQueryBuilder().
Select("*"). Select("*").
From("Posts"). From("Posts").
Where(sq.Or{sq.Gt{"UpdateAt": cursor.LastPostUpdateAt}, sq.And{sq.Eq{"UpdateAt": cursor.LastPostUpdateAt}, sq.Gt{"Id": cursor.LastPostId}}}). Where(sq.Or{sq.Gt{"Posts.UpdateAt": cursor.LastPostUpdateAt}, sq.And{sq.Eq{"Posts.UpdateAt": cursor.LastPostUpdateAt}, sq.Gt{"Posts.Id": cursor.LastPostId}}}).
OrderBy("UpdateAt", "Id"). OrderBy("Posts.UpdateAt", "Id").
Limit(uint64(limit)) Limit(uint64(limit))
if options.ChannelId != "" { if options.ChannelId != "" {
query = query.Where(sq.Eq{"ChannelId": options.ChannelId}) query = query.Where(sq.Eq{"Posts.ChannelId": options.ChannelId})
} }
if !options.IncludeDeleted { if !options.IncludeDeleted {
query = query.Where(sq.Eq{"DeleteAt": 0}) query = query.Where(sq.Eq{"Posts.DeleteAt": 0})
} }
if options.ExcludeRemoteId != "" { if options.ExcludeRemoteId != "" {
@@ -1402,7 +1405,7 @@ func (s *SqlPostStore) getPostsAround(before bool, options model.GetPostsOptions
conditions := sq.And{ conditions := sq.And{
sq.Expr(`CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = ?)`, options.PostId), sq.Expr(`CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = ?)`, options.PostId),
sq.Eq{"p.ChannelId": options.ChannelId}, sq.Eq{"p.ChannelId": options.ChannelId},
sq.Eq{"DeleteAt": int(0)}, sq.Eq{"p.DeleteAt": int(0)},
} }
if options.CollapsedThreads { if options.CollapsedThreads {
conditions = append(conditions, sq.Eq{"RootId": ""}) conditions = append(conditions, sq.Eq{"RootId": ""})
@@ -1415,7 +1418,7 @@ func (s *SqlPostStore) getPostsAround(before bool, options model.GetPostsOptions
// Adding ChannelId and DeleteAt order columns // Adding ChannelId and DeleteAt order columns
// to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always. // to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always.
// See MM-24170. // See MM-24170.
OrderBy("p.ChannelId", "DeleteAt", "CreateAt "+sort). OrderBy("p.ChannelId", "p.DeleteAt", "p.CreateAt "+sort).
Limit(uint64(options.PerPage)). Limit(uint64(options.PerPage)).
Offset(uint64(offset)) Offset(uint64(offset))
@@ -1448,8 +1451,8 @@ func (s *SqlPostStore) getPostsAround(before bool, options model.GetPostsOptions
rootQuery = rootQuery.From("Posts p"). rootQuery = rootQuery.From("Posts p").
Where(sq.And{ Where(sq.And{
idQuery, idQuery,
sq.Eq{"ChannelId": options.ChannelId}, sq.Eq{"p.ChannelId": options.ChannelId},
sq.Eq{"DeleteAt": 0}, sq.Eq{"p.DeleteAt": 0},
}). }).
OrderBy("CreateAt DESC") OrderBy("CreateAt DESC")
@@ -1505,11 +1508,11 @@ func (s *SqlPostStore) getPostIdAroundTime(channelId string, time int64, before
conditions := sq.And{ conditions := sq.And{
direction, direction,
sq.Eq{"ChannelId": channelId}, sq.Eq{"Posts.ChannelId": channelId},
sq.Eq{"DeleteAt": int(0)}, sq.Eq{"Posts.DeleteAt": int(0)},
} }
if collapsedThreads { if collapsedThreads {
conditions = sq.And{conditions, sq.Eq{"RootId": ""}} conditions = sq.And{conditions, sq.Eq{"Posts.RootId": ""}}
} }
query := s.getQueryBuilder(). query := s.getQueryBuilder().
Select("Id"). Select("Id").
@@ -1518,7 +1521,7 @@ func (s *SqlPostStore) getPostIdAroundTime(channelId string, time int64, before
// Adding ChannelId and DeleteAt order columns // Adding ChannelId and DeleteAt order columns
// to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always. // to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always.
// See MM-23369. // See MM-23369.
OrderBy("ChannelId", "DeleteAt", "CreateAt "+sort). OrderBy("Posts.ChannelId", "Posts.DeleteAt", "Posts.CreateAt "+sort).
Limit(1) Limit(1)
queryString, args, err := query.ToSql() queryString, args, err := query.ToSql()
@@ -1545,9 +1548,9 @@ func (s *SqlPostStore) GetPostAfterTime(channelId string, time int64, collapsedT
table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)" table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)"
} }
conditions := sq.And{ conditions := sq.And{
sq.Gt{"CreateAt": time}, sq.Gt{"Posts.CreateAt": time},
sq.Eq{"ChannelId": channelId}, sq.Eq{"Posts.ChannelId": channelId},
sq.Eq{"DeleteAt": int(0)}, sq.Eq{"Posts.DeleteAt": int(0)},
} }
if collapsedThreads { if collapsedThreads {
conditions = sq.And{conditions, sq.Eq{"RootId": ""}} conditions = sq.And{conditions, sq.Eq{"RootId": ""}}
@@ -1559,7 +1562,7 @@ func (s *SqlPostStore) GetPostAfterTime(channelId string, time int64, collapsedT
// Adding ChannelId and DeleteAt order columns // Adding ChannelId and DeleteAt order columns
// to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always. // to let mysql choose the "idx_posts_channel_id_delete_at_create_at" index always.
// See MM-23369. // See MM-23369.
OrderBy("ChannelId", "DeleteAt", "CreateAt ASC"). OrderBy("Posts.ChannelId", "Posts.DeleteAt", "Posts.CreateAt ASC").
Limit(1) Limit(1)
queryString, args, err := query.ToSql() queryString, args, err := query.ToSql()
@@ -1581,9 +1584,9 @@ func (s *SqlPostStore) getRootPosts(channelId string, offset int, limit int, ski
posts := []*model.Post{} posts := []*model.Post{}
var fetchQuery string var fetchQuery string
if skipFetchThreads { if skipFetchThreads {
fetchQuery = "SELECT p.*, (SELECT COUNT(*) 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 = ? AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT ? OFFSET ?" fetchQuery = "SELECT p.*, (SELECT COUNT(*) 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.ChannelId = ? AND p.DeleteAt = 0 ORDER BY p.CreateAt DESC LIMIT ? OFFSET ?"
} else { } else {
fetchQuery = "SELECT * FROM Posts WHERE ChannelId = ? AND DeleteAt = 0 ORDER BY CreateAt DESC LIMIT ? OFFSET ?" fetchQuery = "SELECT * FROM Posts WHERE Posts.ChannelId = ? AND Posts.DeleteAt = 0 ORDER BY Posts.CreateAt DESC LIMIT ? OFFSET ?"
} }
err := s.GetReplicaX().Select(&posts, fetchQuery, channelId, limit, offset) err := s.GetReplicaX().Select(&posts, fetchQuery, channelId, limit, offset)
if err != nil { if err != nil {
@@ -1604,13 +1607,13 @@ func (s *SqlPostStore) getParentsPosts(channelId string, offset int, limit int,
q.RootId q.RootId
FROM FROM
(SELECT (SELECT
RootId Posts.RootId
FROM FROM
Posts Posts
WHERE WHERE
ChannelId = ? Posts.ChannelId = ?
AND DeleteAt = 0 AND Posts.DeleteAt = 0
ORDER BY CreateAt DESC ORDER BY Posts.CreateAt DESC
LIMIT ? OFFSET ?) q LIMIT ? OFFSET ?) q
WHERE q.RootId != ''` WHERE q.RootId != ''`
@@ -1639,10 +1642,10 @@ func (s *SqlPostStore) getParentsPosts(channelId string, offset int, limit int,
From("Posts p"). From("Posts p").
Where(sq.And{ Where(sq.And{
where, where,
sq.Eq{"ChannelId": channelId}, sq.Eq{"p.ChannelId": channelId},
sq.Eq{"DeleteAt": 0}, sq.Eq{"p.DeleteAt": 0},
}). }).
OrderBy("CreateAt") OrderBy("p.CreateAt")
sql, args, err := query.ToSql() sql, args, err := query.ToSql()
if err != nil { if err != nil {
@@ -1675,20 +1678,20 @@ func (s *SqlPostStore) getParentsPostsPostgreSQL(channelId string, offset int, l
q3.RootId q3.RootId
FROM FROM
(SELECT (SELECT
RootId Posts.RootId
FROM FROM
Posts Posts
WHERE WHERE
ChannelId = ? Posts.ChannelId = ?
AND DeleteAt = 0 AND Posts.DeleteAt = 0
ORDER BY CreateAt DESC ORDER BY Posts.CreateAt DESC
LIMIT ? OFFSET ?) q3 LIMIT ? OFFSET ?) q3
WHERE q3.RootId != '') q1 WHERE q3.RootId != '') q1
ON `+onStatement+` ON `+onStatement+`
WHERE WHERE
ChannelId = ? q2.ChannelId = ?
AND DeleteAt = 0 AND q2.DeleteAt = 0
ORDER BY CreateAt`, channelId, limit, offset, channelId) ORDER BY q2.CreateAt`, channelId, limit, offset, channelId)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", channelId) return nil, errors.Wrapf(err, "failed to find Posts with channelId=%s", channelId)
} }
@@ -1835,9 +1838,9 @@ func (s *SqlPostStore) search(teamId string, userId string, params *model.Search
"*", "*",
"(SELECT COUNT(*) FROM Posts WHERE Posts.RootId = (CASE WHEN q2.RootId = '' THEN q2.Id ELSE q2.RootId END) AND Posts.DeleteAt = 0) as ReplyCount", "(SELECT COUNT(*) 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"). ).From("Posts q2").
Where("DeleteAt = 0"). Where("q2.DeleteAt = 0").
Where(fmt.Sprintf("Type NOT LIKE '%s%%'", model.PostSystemMessagePrefix)). Where(fmt.Sprintf("q2.Type NOT LIKE '%s%%'", model.PostSystemMessagePrefix)).
OrderByClause("CreateAt DESC"). OrderByClause("q2.CreateAt DESC").
Limit(100) Limit(100)
var err error var err error
@@ -1928,11 +1931,11 @@ func (s *SqlPostStore) search(teamId string, userId string, params *model.Search
Where("Id = ChannelId") Where("Id = ChannelId")
if !params.IncludeDeletedChannels { if !params.IncludeDeletedChannels {
inQuery = inQuery.Where("DeleteAt = 0") inQuery = inQuery.Where("Channels.DeleteAt = 0")
} }
if !params.SearchWithoutUserId { if !params.SearchWithoutUserId {
inQuery = inQuery.Where("UserId = ?", userId) inQuery = inQuery.Where("ChannelMembers.UserId = ?", userId)
} }
inQuery = s.buildSearchTeamFilterClause(teamId, inQuery) inQuery = s.buildSearchTeamFilterClause(teamId, inQuery)
@@ -2378,10 +2381,10 @@ func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string) ([]*m
FROM FROM
Posts Posts
WHERE WHERE
Id > ? Posts.Id > ?
AND RootId = '' AND Posts.RootId = ''
AND DeleteAt = 0 AND Posts.DeleteAt = 0
ORDER BY Id ORDER BY Posts.Id
LIMIT ?`, LIMIT ?`,
afterId, limit) afterId, limit)
if err != nil { if err != nil {
@@ -2395,7 +2398,7 @@ func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string) ([]*m
builder := s.getQueryBuilder(). builder := s.getQueryBuilder().
Select("p1.*, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName"). Select("p1.*, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName").
FromSelect(sq.Select("*").From("Posts").Where(sq.Eq{"Id": rootIds}), "p1"). FromSelect(sq.Select("*").From("Posts").Where(sq.Eq{"Posts.Id": rootIds}), "p1").
InnerJoin("Channels ON p1.ChannelId = Channels.Id"). InnerJoin("Channels ON p1.ChannelId = Channels.Id").
InnerJoin("Teams ON Channels.TeamId = Teams.Id"). InnerJoin("Teams ON Channels.TeamId = Teams.Id").
InnerJoin("Users ON p1.UserId = Users.Id"). InnerJoin("Users ON p1.UserId = Users.Id").
@@ -2598,19 +2601,36 @@ func (s *SqlPostStore) permanentDeleteThreads(transaction *sqlxTxWrapper, postId
return nil return nil
} }
// Thread cleanup upon post deletion // deleteThread marks a thread as deleted at the given time.
// if the post is a comment func (s *SqlPostStore) deleteThread(transaction *sqlxTxWrapper, postId string, deleteAtTime int64) error {
// reply count is reduced by 1 and, queryString, args, err := s.getQueryBuilder().
// the user is removed from participants if the comment deleted is the last reply from said user. Update("Threads").
func (s *SqlPostStore) cleanupThreadComments(transaction *sqlxTxWrapper, postId, rootId string, userId string) error { Set("DeleteAt", deleteAtTime).
Where(sq.Eq{"PostId": postId}).
ToSql()
if err != nil {
return errors.Wrapf(err, "failed to create SQL query to mark thread for root post %s as deleted", postId)
}
_, err = transaction.Exec(queryString, args...)
if err != nil {
return errors.Wrapf(err, "failed to mark thread for root post %s as deleted", postId)
}
return nil
}
// updateThreadAfterReplyDeletion decrements the thread reply count and adjusts the participants
// list as necessary.
func (s *SqlPostStore) updateThreadAfterReplyDeletion(transaction *sqlxTxWrapper, rootId string, userId string) error {
if rootId != "" { if rootId != "" {
queryString, args, err := s.getQueryBuilder(). queryString, args, err := s.getQueryBuilder().
Select("COUNT(Id)"). Select("COUNT(Posts.Id)").
From("Posts"). From("Posts").
Where(sq.And{ Where(sq.And{
sq.Eq{"RootId": rootId}, sq.Eq{"Posts.RootId": rootId},
sq.Eq{"UserId": userId}, sq.Eq{"Posts.UserId": userId},
sq.Eq{"DeleteAt": 0}, sq.Eq{"Posts.DeleteAt": 0},
}). }).
ToSql() ToSql()
@@ -2675,9 +2695,16 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts
return nil return nil
} }
threadsByRootsSql, threadsByRootsArgs, _ := s.getQueryBuilder(). threadsByRootsSql, threadsByRootsArgs, _ := s.getQueryBuilder().
Select("*"). Select(
"Threads.PostId",
"Threads.ChannelId",
"Threads.ReplyCount",
"Threads.LastReplyAt",
"Threads.Participants",
"COALESCE(Threads.DeleteAt, 0) AS DeleteAt",
).
From("Threads"). From("Threads").
Where(sq.Eq{"PostId": rootIds}). Where(sq.Eq{"Threads.PostId": rootIds}).
ToSql() ToSql()
threadsByRoots := []*model.Thread{} threadsByRoots := []*model.Thread{}
if err := transaction.Select(&threadsByRoots, threadsByRootsSql, threadsByRootsArgs...); err != nil { if err := transaction.Select(&threadsByRoots, threadsByRootsSql, threadsByRootsArgs...); err != nil {
@@ -2697,7 +2724,7 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts
}{} }{}
// calculate participants // calculate participants
if err := transaction.Select(&data, "SELECT UserId, MAX(CreateAt) as RepliedAt FROM Posts WHERE RootId=? AND DeleteAt=0 GROUP BY UserId ORDER BY RepliedAt ASC", rootId); err != nil { if err := transaction.Select(&data, "SELECT Posts.UserId, MAX(Posts.CreateAt) as RepliedAt FROM Posts WHERE Posts.RootId=? AND Posts.DeleteAt=0 GROUP BY Posts.UserId ORDER BY RepliedAt ASC", rootId); err != nil {
return err return err
} }
@@ -2708,13 +2735,13 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts
// calculate reply count // calculate reply count
var count int64 var count int64
err := transaction.Get(&count, "SELECT COUNT(Id) FROM Posts WHERE RootId=? And DeleteAt=0", rootId) err := transaction.Get(&count, "SELECT COUNT(Posts.Id) FROM Posts WHERE Posts.RootId=? And Posts.DeleteAt=0", rootId)
if err != nil { if err != nil {
return err return err
} }
// calculate last reply at // calculate last reply at
var lastReplyAt int64 var lastReplyAt int64
err = transaction.Get(&lastReplyAt, "SELECT COALESCE(MAX(Posts.CreateAt), 0) FROM Posts WHERE RootID=? and DeleteAt=0", rootId) err = transaction.Get(&lastReplyAt, "SELECT COALESCE(MAX(Posts.CreateAt), 0) FROM Posts WHERE Posts.RootID=? and Posts.DeleteAt=0", rootId)
if err != nil { if err != nil {
return err return err
} }

Просмотреть файл

@@ -20,28 +20,63 @@ import (
type SqlThreadStore struct { type SqlThreadStore struct {
*SqlStore *SqlStore
// threadsSelectQuery is for querying directly into model.Thread
threadsSelectQuery sq.SelectBuilder
// threadsAndPostsSelectQuery is for querying into a struct embedding fields from
// model.Thread and model.Post.
threadsAndPostsSelectQuery sq.SelectBuilder
} }
func (s *SqlThreadStore) ClearCaches() { func (s *SqlThreadStore) ClearCaches() {
} }
func newSqlThreadStore(sqlStore *SqlStore) store.ThreadStore { func newSqlThreadStore(sqlStore *SqlStore) store.ThreadStore {
return &SqlThreadStore{ s := SqlThreadStore{
SqlStore: sqlStore, SqlStore: sqlStore,
} }
s.initializeQueries()
return &s
}
func (s *SqlThreadStore) initializeQueries() {
s.threadsSelectQuery = s.getQueryBuilder().
Select(
"Threads.PostId",
"Threads.ChannelId",
"Threads.ReplyCount",
"Threads.LastReplyAt",
"Threads.Participants",
"COALESCE(Threads.DeleteAt, 0) AS DeleteAt",
).
From("Threads")
s.threadsAndPostsSelectQuery = s.getQueryBuilder().
Select(
"Threads.PostId",
"Threads.ChannelId",
"Threads.ReplyCount",
"Threads.LastReplyAt",
"Threads.Participants",
"COALESCE(Threads.DeleteAt, 0) AS ThreadDeleteAt",
).
From("Threads")
} }
func (s *SqlThreadStore) Get(id string) (*model.Thread, error) { func (s *SqlThreadStore) Get(id string) (*model.Thread, error) {
var thread model.Thread var thread model.Thread
query, args, err := s.getQueryBuilder().
Select("*"). query, args, err := s.threadsSelectQuery.
From("Threads").
Where(sq.Eq{"PostId": id}). Where(sq.Eq{"PostId": id}).
ToSql() ToSql()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "thread_tosql") return nil, errors.Wrap(err, "thread_tosql")
} }
err = s.GetMasterX().Get(&thread, query, args...)
err = s.GetReplicaX().Get(&thread, query, args...)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, nil return nil, nil
@@ -52,55 +87,7 @@ func (s *SqlThreadStore) Get(id string) (*model.Thread, error) {
return &thread, nil return &thread, nil
} }
// GetTotalUnreadThreads counts the number of unread threads for the given user, optionally func (s *SqlThreadStore) getTotalThreadsQuery(userId, teamId string, opts model.GetUserThreadsOpts) sq.SelectBuilder {
// constrained to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalUnreadThreads(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
var totalUnreadThreads int64
query := s.getQueryBuilder().
Select("COUNT(DISTINCT(Posts.RootId))").
From("Posts").
LeftJoin("ThreadMemberships ON Posts.RootId = ThreadMemberships.PostId").
Where("Posts.CreateAt > ThreadMemberships.LastViewed").
Where(sq.Eq{
"ThreadMemberships.UserId": userId,
"ThreadMemberships.Following": true,
})
if teamId != "" {
query = query.
LeftJoin("Channels ON Posts.ChannelId = Channels.Id").
Where(sq.Or{
sq.Eq{"Channels.TeamId": teamId},
sq.Eq{"Channels.TeamId": ""},
})
}
if !opts.Deleted {
query = query.Where(sq.Eq{"COALESCE(Posts.DeleteAt, 0)": 0})
}
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrapf(err, "failed to build query to count unread threads for user id=%s", userId)
}
err = s.GetMasterX().Get(&totalUnreadThreads, sql, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread threads for user id=%s", userId)
}
return totalUnreadThreads, nil
}
// GetTotalUnreadThreads counts the number of threads for the given user, optionally constrained
// to the given team + DMs/GMs.
//
// TODO: Why do we support an Unread flag here? It's basically the same as GetTotalUnreadThreads,
// but with different comparison semantics.
func (s *SqlThreadStore) GetTotalThreads(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
var totalCount int64
query := s.getQueryBuilder(). query := s.getQueryBuilder().
Select("COUNT(ThreadMemberships.PostId)"). Select("COUNT(ThreadMemberships.PostId)").
From("ThreadMemberships"). From("ThreadMemberships").
@@ -120,27 +107,53 @@ func (s *SqlThreadStore) GetTotalThreads(userId, teamId string, opts model.GetUs
} }
if !opts.Deleted { if !opts.Deleted {
query = query. query = query.Where(sq.Eq{"COALESCE(Threads.DeleteAt, 0)": 0})
LeftJoin("Posts ON Posts.Id = ThreadMemberships.PostId").
Where(sq.Eq{"COALESCE(Posts.DeleteAt, 0)": 0})
} }
if opts.Unread { return query
query = query. }
Where(sq.Expr("ThreadMemberships.LastViewed < Threads.LastReplyAt"))
// GetTotalUnreadThreads counts the number of unread threads for the given user, optionally
// constrained to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalUnreadThreads(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
query := s.getTotalThreadsQuery(userId, teamId, opts).
Where(sq.Expr("ThreadMemberships.LastViewed < Threads.LastReplyAt"))
sql, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrapf(err, "failed to build query to count unread threads for user id=%s", userId)
} }
var totalUnreadThreads int64
err = s.GetReplicaX().Get(&totalUnreadThreads, sql, args...)
if err != nil {
return 0, errors.Wrapf(err, "failed to count unread threads for user id=%s", userId)
}
return totalUnreadThreads, nil
}
// GetTotalUnreadThreads counts the number of threads for the given user, optionally constrained
// to the given team + DMs/GMs.
func (s *SqlThreadStore) GetTotalThreads(userId, teamId string, opts model.GetUserThreadsOpts) (int64, error) {
if opts.Unread {
return 0, errors.New("GetTotalThreads does not support the Unread flag; use GetTotalUnreadThreads instead")
}
query := s.getTotalThreadsQuery(userId, teamId, opts)
sql, args, err := query.ToSql() sql, args, err := query.ToSql()
if err != nil { if err != nil {
return 0, errors.Wrapf(err, "failed to build query to count threads for user id=%s", userId) return 0, errors.Wrapf(err, "failed to build query to count threads for user id=%s", userId)
} }
err = s.GetMasterX().Get(&totalCount, sql, args...) var totalThreads int64
err = s.GetReplicaX().Get(&totalThreads, sql, args...)
if err != nil { if err != nil {
return 0, errors.Wrapf(err, "failed to count threads for user id=%s", userId) return 0, errors.Wrapf(err, "failed to count threads for user id=%s", userId)
} }
return totalCount, nil return totalThreads, nil
} }
// GetTotalUnreadMentions counts the number of unread mentions for the given user, optionally // GetTotalUnreadMentions counts the number of unread mentions for the given user, optionally
@@ -151,6 +164,7 @@ func (s *SqlThreadStore) GetTotalUnreadMentions(userId, teamId string, opts mode
query := s.getQueryBuilder(). query := s.getQueryBuilder().
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0)"). Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0)").
From("ThreadMemberships"). From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
Where(sq.Eq{ Where(sq.Eq{
"ThreadMemberships.UserId": userId, "ThreadMemberships.UserId": userId,
"ThreadMemberships.Following": true, "ThreadMemberships.Following": true,
@@ -158,7 +172,6 @@ func (s *SqlThreadStore) GetTotalUnreadMentions(userId, teamId string, opts mode
if teamId != "" { if teamId != "" {
query = query. query = query.
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
LeftJoin("Channels ON Threads.ChannelId = Channels.Id"). LeftJoin("Channels ON Threads.ChannelId = Channels.Id").
Where(sq.Or{ Where(sq.Or{
sq.Eq{"Channels.TeamId": teamId}, sq.Eq{"Channels.TeamId": teamId},
@@ -167,9 +180,7 @@ func (s *SqlThreadStore) GetTotalUnreadMentions(userId, teamId string, opts mode
} }
if !opts.Deleted { if !opts.Deleted {
query = query. query = query.Where(sq.Eq{"COALESCE(Threads.DeleteAt, 0)": 0})
LeftJoin("Posts ON Posts.Id = ThreadMemberships.PostId").
Where(sq.Eq{"COALESCE(Posts.DeleteAt, 0)": 0})
} }
sql, args, err := query.ToSql() sql, args, err := query.ToSql()
@@ -177,7 +188,7 @@ func (s *SqlThreadStore) GetTotalUnreadMentions(userId, teamId string, opts mode
return 0, errors.Wrapf(err, "failed to build query to count unread mentions for user id=%s", userId) return 0, errors.Wrapf(err, "failed to build query to count unread mentions for user id=%s", userId)
} }
err = s.GetMasterX().Get(&totalUnreadMentions, sql, args...) err = s.GetReplicaX().Get(&totalUnreadMentions, sql, args...)
if err != nil { if err != nil {
return 0, errors.Wrapf(err, "failed to count unread mentions for user id=%s", userId) return 0, errors.Wrapf(err, "failed to count unread mentions for user id=%s", userId)
} }
@@ -199,6 +210,7 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get
UnreadReplies int64 UnreadReplies int64
UnreadMentions int64 UnreadMentions int64
Participants model.StringArray Participants model.StringArray
ThreadDeleteAt int64
model.Post model.Post
} }
@@ -217,12 +229,12 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get
return nil, errors.Wrapf(err, "failed to build subquery to count unread replies when getting threads for user id=%s", userId) return nil, errors.Wrapf(err, "failed to build subquery to count unread replies when getting threads for user id=%s", userId)
} }
query := s.getQueryBuilder(). query := s.threadsAndPostsSelectQuery.
Select(`Threads.*, Column(postSliceCoalesceQuery()).
` + postSliceCoalesceQuery() + `, Columns(
ThreadMemberships.LastViewed as LastViewedAt, "ThreadMemberships.LastViewed as LastViewedAt",
ThreadMemberships.UnreadMentions as UnreadMentions`). "ThreadMemberships.UnreadMentions as UnreadMentions",
From("Threads"). ).
Column(sq.Alias(sq.Expr(unreadRepliesSql, unreadRepliesArgs...), "UnreadReplies")). Column(sq.Alias(sq.Expr(unreadRepliesSql, unreadRepliesArgs...), "UnreadReplies")).
Join("Posts ON Posts.Id = Threads.PostId"). Join("Posts ON Posts.Id = Threads.PostId").
Join("ThreadMemberships ON ThreadMemberships.PostId = Threads.PostId") Join("ThreadMemberships ON ThreadMemberships.PostId = Threads.PostId")
@@ -235,7 +247,7 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get
// a team at all. // a team at all.
if teamId != "" { if teamId != "" {
query = query. query = query.
Join("Channels ON Posts.ChannelId = Channels.Id"). Join("Channels ON Threads.ChannelId = Channels.Id").
Where(sq.Or{ Where(sq.Or{
sq.Eq{"Channels.TeamId": teamId}, sq.Eq{"Channels.TeamId": teamId},
sq.Eq{"Channels.TeamId": ""}, sq.Eq{"Channels.TeamId": ""},
@@ -243,7 +255,10 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get
} }
if !opts.Deleted { if !opts.Deleted {
query = query.Where(sq.Eq{"Posts.DeleteAt": 0}) query = query.Where(sq.Or{
sq.Eq{"Threads.DeleteAt": nil},
sq.Eq{"Threads.DeleteAt": 0},
})
} }
if opts.Since > 0 { if opts.Since > 0 {
@@ -256,11 +271,11 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get
order := "DESC" order := "DESC"
if opts.Before != "" { if opts.Before != "" {
query = query.Where(sq.Expr(`LastReplyAt < (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.Before)) query = query.Where(sq.Expr(`Threads.LastReplyAt < (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.Before))
} }
if opts.After != "" { if opts.After != "" {
order = "ASC" order = "ASC"
query = query.Where(sq.Expr(`LastReplyAt > (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.After)) query = query.Where(sq.Expr(`Threads.LastReplyAt > (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.After))
} }
query = query. query = query.
@@ -323,6 +338,7 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get
UnreadMentions: thread.UnreadMentions, UnreadMentions: thread.UnreadMentions,
Participants: threadParticipants, Participants: threadParticipants,
Post: thread.Post.ToNilIfInvalid(), Post: thread.Post.ToNilIfInvalid(),
DeleteAt: thread.ThreadDeleteAt,
}) })
} }
@@ -336,7 +352,7 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string)
sq.Eq{"ThreadMemberships.UserId": userID}, sq.Eq{"ThreadMemberships.UserId": userID},
sq.Eq{"ThreadMemberships.Following": true}, sq.Eq{"ThreadMemberships.Following": true},
sq.Eq{"Channels.TeamId": teamIDs}, sq.Eq{"Channels.TeamId": teamIDs},
sq.Eq{"COALESCE(Posts.DeleteAt, 0)": 0}, sq.Eq{"COALESCE(Threads.DeleteAt, 0)": 0},
} }
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -362,7 +378,6 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string)
From("Threads"). From("Threads").
LeftJoin("ThreadMemberships ON Threads.PostId = ThreadMemberships.PostId"). LeftJoin("ThreadMemberships ON Threads.PostId = ThreadMemberships.PostId").
LeftJoin("Channels ON Threads.ChannelId = Channels.Id"). LeftJoin("Channels ON Threads.ChannelId = Channels.Id").
LeftJoin("Posts ON Posts.Id = Threads.PostId").
Where(fetchConditions). Where(fetchConditions).
Where("Threads.LastReplyAt > ThreadMemberships.LastViewed"). Where("Threads.LastReplyAt > ThreadMemberships.LastViewed").
GroupBy("Channels.TeamId"). GroupBy("Channels.TeamId").
@@ -385,7 +400,6 @@ func (s *SqlThreadStore) GetTeamsUnreadForUser(userID string, teamIDs []string)
Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0) AS Count, TeamId"). Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0) AS Count, TeamId").
From("ThreadMemberships"). From("ThreadMemberships").
LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId"). LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
LeftJoin("Posts ON Posts.Id = ThreadMemberships.PostId").
LeftJoin("Channels ON Threads.ChannelId = Channels.Id"). LeftJoin("Channels ON Threads.ChannelId = Channels.Id").
Where(fetchConditions). Where(fetchConditions).
GroupBy("Channels.TeamId"). GroupBy("Channels.TeamId").
@@ -476,6 +490,7 @@ func (s *SqlThreadStore) GetThreadForUser(teamId string, threadMembership *model
UnreadReplies int64 UnreadReplies int64
UnreadMentions int64 UnreadMentions int64
Participants model.StringArray Participants model.StringArray
ThreadDeleteAt int64
model.Post model.Post
} }
@@ -496,21 +511,26 @@ func (s *SqlThreadStore) GetThreadForUser(teamId string, threadMembership *model
sq.Eq{"Threads.PostId": threadMembership.PostId}, sq.Eq{"Threads.PostId": threadMembership.PostId},
} }
query := s.threadsAndPostsSelectQuery
for _, c := range postSliceColumns() {
query = query.Column("Posts." + c)
}
var thread JoinedThread var thread JoinedThread
query, threadArgs, err := s.getQueryBuilder(). querySQL, threadArgs, err := query.
Select("Threads.*, Posts.*").
From("Threads").
Column(sq.Alias(sq.Expr(unreadRepliesQuery), "UnreadReplies")). Column(sq.Alias(sq.Expr(unreadRepliesQuery), "UnreadReplies")).
LeftJoin("Posts ON Posts.Id = Threads.PostId"). LeftJoin("Posts ON Posts.Id = Threads.PostId").
LeftJoin("Channels ON Posts.ChannelId = Channels.Id"). LeftJoin("Channels ON Posts.ChannelId = Channels.Id").
Where(fetchConditions).ToSql() Where(fetchConditions).
ToSql()
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to build query to get thread for user id=%s, post id=%s", threadMembership.UserId, threadMembership.PostId) return nil, errors.Wrapf(err, "failed to build query to get thread for user id=%s, post id=%s", threadMembership.UserId, threadMembership.PostId)
} }
args := append(unreadRepliesArgs, threadArgs...) args := append(unreadRepliesArgs, threadArgs...)
err = s.GetReplicaX().Get(&thread, query, args...) err = s.GetReplicaX().Get(&thread, querySQL, args...)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Thread", threadMembership.PostId) return nil, store.NewErrNotFound("Thread", threadMembership.PostId)
@@ -557,6 +577,7 @@ func (s *SqlThreadStore) GetThreadForUser(teamId string, threadMembership *model
UnreadMentions: thread.UnreadMentions, UnreadMentions: thread.UnreadMentions,
Participants: participants, Participants: participants,
Post: thread.Post.ToNilIfInvalid(), Post: thread.Post.ToNilIfInvalid(),
DeleteAt: thread.ThreadDeleteAt,
} }
return result, nil return result, nil

Просмотреть файл

@@ -27,8 +27,6 @@ func TestPostStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("GetSingle", func(t *testing.T) { testPostStoreGetSingle(t, ss) }) t.Run("GetSingle", func(t *testing.T) { testPostStoreGetSingle(t, ss) })
t.Run("Update", func(t *testing.T) { testPostStoreUpdate(t, ss) }) t.Run("Update", func(t *testing.T) { testPostStoreUpdate(t, ss) })
t.Run("Delete", func(t *testing.T) { testPostStoreDelete(t, ss) }) t.Run("Delete", func(t *testing.T) { testPostStoreDelete(t, ss) })
t.Run("Delete1Level", func(t *testing.T) { testPostStoreDelete1Level(t, ss) })
t.Run("Delete2Level", func(t *testing.T) { testPostStoreDelete2Level(t, ss) })
t.Run("PermDelete1Level", func(t *testing.T) { testPostStorePermDelete1Level(t, ss) }) t.Run("PermDelete1Level", func(t *testing.T) { testPostStorePermDelete1Level(t, ss) })
t.Run("PermDelete1Level2", func(t *testing.T) { testPostStorePermDelete1Level2(t, ss) }) t.Run("PermDelete1Level2", func(t *testing.T) { testPostStorePermDelete1Level2(t, ss) })
t.Run("GetWithChildren", func(t *testing.T) { testPostStoreGetWithChildren(t, ss) }) t.Run("GetWithChildren", func(t *testing.T) { testPostStoreGetWithChildren(t, ss) })
@@ -317,6 +315,9 @@ func testPostStoreSaveMultiple(t *testing.T, ss store.Store) {
replyPost3.Message = NewTestId() replyPost3.Message = NewTestId()
replyPost3.RootId = rootPost.Id replyPost3.RootId = rootPost.Id
// Ensure update does not occur in the same timestamp as creation
time.Sleep(time.Millisecond)
_, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost2, &replyPost3}) _, _, err = ss.Post().SaveMultiple([]*model.Post{&replyPost2, &replyPost3})
require.NoError(t, err) require.NoError(t, err)
@@ -819,109 +820,132 @@ func testPostStoreUpdate(t *testing.T, ss store.Store) {
} }
func testPostStoreDelete(t *testing.T, ss store.Store) { func testPostStoreDelete(t *testing.T, ss store.Store) {
o1 := &model.Post{} t.Run("single post, no replies", func(t *testing.T) {
o1.ChannelId = model.NewId() // Create a post
o1.UserId = model.NewId() rootPost, err := ss.Post().Save(&model.Post{
o1.Message = model.NewRandomString(10) ChannelId: model.NewId(),
deleteByID := model.NewId() UserId: model.NewId(),
Message: model.NewRandomString(10),
})
require.NoError(t, err)
etag1 := ss.Post().GetEtag(o1.ChannelId, false, false) // Verify etag generation for the channel containing the post.
require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag") etag1 := ss.Post().GetEtag(rootPost.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag")
o1, err := ss.Post().Save(o1) // Verify the created post.
require.NoError(t, err) r1, err := ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "")
require.NoError(t, err)
require.NotNil(t, r1.Posts[rootPost.Id])
require.Equal(t, rootPost, r1.Posts[rootPost.Id])
r1, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "") // Mark the post as deleted by the user identified with deleteByID.
require.NoError(t, err) deleteByID := model.NewId()
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post") err = ss.Post().Delete(rootPost.Id, model.GetMillis(), deleteByID)
require.NoError(t, err)
err = ss.Post().Delete(o1.Id, model.GetMillis(), deleteByID) // Ensure the appropriate posts prop reflects the user deleting the post.
require.NoError(t, err) posts, err := ss.Post().GetPostsCreatedAt(rootPost.ChannelId, rootPost.CreateAt)
require.NoError(t, err)
require.NotEmpty(t, posts)
assert.Equal(t, deleteByID, posts[0].GetProp(model.PostPropsDeleteBy), "unexpected Props[model.PostPropsDeleteBy]")
posts, _ := ss.Post().GetPostsCreatedAt(o1.ChannelId, o1.CreateAt) // Verify that the post is no longer fetched by default.
post := posts[0] _, err = ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "")
actual := post.GetProp(model.PostPropsDeleteBy) require.Error(t, err, "fetching deleted post should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
assert.Equal(t, deleteByID, actual, "Expected (*Post).Props[model.PostPropsDeleteBy] to be %v but got %v.", deleteByID, actual) // Verify etag generation for the channel containing the now deleted post.
etag2 := ss.Post().GetEtag(rootPost.ChannelId, false, false)
require.Equal(t, 0, strings.Index(etag2, model.CurrentVersion+"."), "Invalid Etag")
})
r3, err := ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "") t.Run("thread with one reply", func(t *testing.T) {
require.Error(t, err, "Missing id should have failed - PostList %v", r3) // Create a root post
rootPost, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
etag2 := ss.Post().GetEtag(o1.ChannelId, false, false) // Reply to that root post
require.Equal(t, 0, strings.Index(etag2, model.CurrentVersion+"."), "Invalid Etag") replyPost, err := ss.Post().Save(&model.Post{
} ChannelId: rootPost.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost.Id,
})
require.NoError(t, err)
func testPostStoreDelete1Level(t *testing.T, ss store.Store) { // Delete the root post
o1 := &model.Post{} err = ss.Post().Delete(rootPost.Id, model.GetMillis(), "")
o1.ChannelId = model.NewId() require.NoError(t, err)
o1.UserId = model.NewId()
o1.Message = NewTestId()
o1, err := ss.Post().Save(o1)
require.NoError(t, err)
o2 := &model.Post{} // Verify the root post deleted
o2.ChannelId = o1.ChannelId _, err = ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "")
o2.UserId = model.NewId() require.Error(t, err, "Deleted id should have failed")
o2.Message = NewTestId() require.IsType(t, &store.ErrNotFound{}, err)
o2.RootId = o1.Id
o2, err = ss.Post().Save(o2)
require.NoError(t, err)
err = ss.Post().Delete(o1.Id, model.GetMillis(), "") // Verify the reply post deleted
require.NoError(t, err) _, err = ss.Post().Get(context.Background(), replyPost.Id, model.GetPostsOptions{}, "")
require.Error(t, err, "Deleted id should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
})
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "") t.Run("thread with multiple replies", func(t *testing.T) {
require.Error(t, err, "Deleted id should have failed") // Create a root post
rootPost1, err := ss.Post().Save(&model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: NewTestId(),
})
require.NoError(t, err)
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "") // Reply to that root post
require.Error(t, err, "Deleted id should have failed") replyPost1, err := ss.Post().Save(&model.Post{
} ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestId(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
func testPostStoreDelete2Level(t *testing.T, ss store.Store) { // Reply to that root post a second time
o1 := &model.Post{} replyPost2, err := ss.Post().Save(&model.Post{
o1.ChannelId = model.NewId() ChannelId: rootPost1.ChannelId,
o1.UserId = model.NewId() UserId: model.NewId(),
o1.Message = NewTestId() Message: NewTestId(),
o1, err := ss.Post().Save(o1) RootId: rootPost1.Id,
require.NoError(t, err) })
require.NoError(t, err)
o2 := &model.Post{} // Create another root post in a separate channel
o2.ChannelId = o1.ChannelId rootPost2, err := ss.Post().Save(&model.Post{
o2.UserId = model.NewId() ChannelId: model.NewId(),
o2.Message = NewTestId() UserId: model.NewId(),
o2.RootId = o1.Id Message: NewTestId(),
o2, err = ss.Post().Save(o2) })
require.NoError(t, err) require.NoError(t, err)
o3 := &model.Post{} // Delete the root post
o3.ChannelId = o1.ChannelId err = ss.Post().Delete(rootPost1.Id, model.GetMillis(), "")
o3.UserId = model.NewId() require.NoError(t, err)
o3.Message = NewTestId()
o3.RootId = o1.Id
o3, err = ss.Post().Save(o3)
require.NoError(t, err)
o4 := &model.Post{} // Verify the root post and replies deleted
o4.ChannelId = model.NewId() _, err = ss.Post().Get(context.Background(), rootPost1.Id, model.GetPostsOptions{}, "")
o4.UserId = model.NewId() require.Error(t, err, "Deleted id should have failed")
o4.Message = NewTestId()
o4, err = ss.Post().Save(o4)
require.NoError(t, err)
err = ss.Post().Delete(o1.Id, model.GetMillis(), "") _, err = ss.Post().Get(context.Background(), replyPost1.Id, model.GetPostsOptions{}, "")
require.NoError(t, err) require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "") _, err = ss.Post().Get(context.Background(), replyPost2.Id, model.GetPostsOptions{}, "")
require.Error(t, err, "Deleted id should have failed") require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "") // Verify other root posts remain undeleted.
require.Error(t, err, "Deleted id should have failed") _, err = ss.Post().Get(context.Background(), rootPost2.Id, model.GetPostsOptions{}, "")
require.NoError(t, err)
_, err = ss.Post().Get(context.Background(), o3.Id, model.GetPostsOptions{}, "") })
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(context.Background(), o4.Id, model.GetPostsOptions{}, "")
require.NoError(t, err)
} }
func testPostStorePermDelete1Level(t *testing.T, ss store.Store) { func testPostStorePermDelete1Level(t *testing.T, ss store.Store) {

Просмотреть файл

@@ -684,6 +684,12 @@ func testGetTeamsUnreadForUser(t *testing.T, ss store.Store) {
assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadMentionCount) assert.Equal(t, int64(1), teamsUnread[team2.Id].ThreadMentionCount)
} }
type byPostId []*model.Post
func (a byPostId) Len() int { return len(a) }
func (a byPostId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byPostId) Less(i, j int) bool { return a[i].Id < a[j].Id }
func testVarious(t *testing.T, ss store.Store) { func testVarious(t *testing.T, ss store.Store) {
createThreadMembership := func(userID, postID string, isMention bool) { createThreadMembership := func(userID, postID string, isMention bool) {
t.Helper() t.Helper()
@@ -818,6 +824,16 @@ func testVarious(t *testing.T, ss store.Store) {
}) })
require.NoError(t, err) require.NoError(t, err)
postNames := map[string]string{
team1channel1post1.Id: "team1channel1post1",
team1channel1post2.Id: "team1channel1post2",
team1channel1post3.Id: "team1channel1post3",
team2channel1post1.Id: "team2channel1post1",
team2channel1post2deleted.Id: "team2channel1post2deleted",
dm1post1.Id: "dm1post1",
gm1post1.Id: "gm1post1",
}
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post1.Id, user2ID, model.GetMillis()) threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post1.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis()) threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis())
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post3.Id, user2ID, model.GetMillis()) threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post3.Id, user2ID, model.GetMillis())
@@ -845,9 +861,37 @@ func testVarious(t *testing.T, ss store.Store) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis()) threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis())
// Actually make team2channel1post2deleted deleted
err = ss.Post().Delete(team2channel1post2deleted.Id, model.GetMillis(), user1ID) err = ss.Post().Delete(team2channel1post2deleted.Id, model.GetMillis(), user1ID)
require.NoError(t, err) require.NoError(t, err)
// Re-fetch posts to ensure metadata up-to-date
allPosts := []*model.Post{
team1channel1post1,
team1channel1post2,
team1channel1post3,
team2channel1post1,
team2channel1post2deleted,
dm1post1,
gm1post1,
}
for i := range allPosts {
updatedPost, err := ss.Post().GetSingle(allPosts[i].Id, true)
require.NoError(t, err)
// Fix some inconsistencies with how the post store returns posts vs. how the
// thread store returns it.
if updatedPost.RemoteId == nil {
updatedPost.RemoteId = new(string)
}
// Also, we don't populate ReplyCount for posts when querying threads, so don't
// assert same.
updatedPost.ReplyCount = 0
updatedPost.ShallowCopy(allPosts[i])
}
t.Run("GetTotalUnreadThreads", func(t *testing.T) { t.Run("GetTotalUnreadThreads", func(t *testing.T) {
testCases := []struct { testCases := []struct {
Description string Description string
@@ -899,27 +943,15 @@ func testVarious(t *testing.T, ss store.Store) {
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{ {"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1, team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1,
}}, }},
{"team1, user1, unread", user1ID, team1.Id, model.GetUserThreadsOpts{Unread: true}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1,
}},
{"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{ {"team1, user1, deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1, // (no deleted threads in team1) team1channel1post1, team1channel1post2, team1channel1post3, dm1post1, gm1post1, // (no deleted threads in team1)
}}, }},
{"team1, user1, unread + deleted", user1ID, team1.Id, model.GetUserThreadsOpts{Unread: true, Deleted: true}, []*model.Post{
team1channel1post2, team1channel1post3, gm1post1, // (no deleted threads in team1)
}},
{"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{ {"team2, user1", user1ID, team2.Id, model.GetUserThreadsOpts{}, []*model.Post{
team2channel1post1, dm1post1, gm1post1, team2channel1post1, dm1post1, gm1post1,
}}, }},
{"team2, user1, unread", user1ID, team2.Id, model.GetUserThreadsOpts{Unread: true}, []*model.Post{
gm1post1, // (no unread in team2)
}},
{"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{ {"team2, user1, deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Deleted: true}, []*model.Post{
team2channel1post1, team2channel1post2deleted, dm1post1, gm1post1, team2channel1post1, team2channel1post2deleted, dm1post1, gm1post1,
}}, }},
{"team2, user1, unread + deleted", user1ID, team2.Id, model.GetUserThreadsOpts{Unread: true, Deleted: true}, []*model.Post{
team2channel1post2deleted, gm1post1,
}},
} }
for _, testCase := range testCases { for _, testCase := range testCases {
@@ -962,6 +994,46 @@ func testVarious(t *testing.T, ss store.Store) {
} }
}) })
assertThreadPosts := func(t *testing.T, threads []*model.ThreadResponse, expectedPosts []*model.Post) {
t.Helper()
actualPosts := make([]*model.Post, 0, len(threads))
actualPostNames := make([]string, 0, len(threads))
for _, thread := range threads {
actualPosts = append(actualPosts, thread.Post)
postName, ok := postNames[thread.PostId]
require.True(t, ok, "failed to find actual %s in post names", thread.PostId)
actualPostNames = append(actualPostNames, postName)
}
sort.Strings(actualPostNames)
expectedPostNames := make([]string, 0, len(expectedPosts))
for _, post := range expectedPosts {
postName, ok := postNames[post.Id]
require.True(t, ok, "failed to find expected %s in post names", post.Id)
expectedPostNames = append(expectedPostNames, postName)
}
sort.Strings(expectedPostNames)
assert.Equal(t, expectedPostNames, actualPostNames)
// Check posts themselves
sort.Sort(byPostId(expectedPosts))
sort.Sort(byPostId(actualPosts))
if assert.Len(t, actualPosts, len(expectedPosts)) {
for i := range actualPosts {
assert.Equal(t, expectedPosts[i], actualPosts[i], "mismatch comparing expected post %s with actual post %s", postNames[expectedPosts[i].Id], postNames[actualPosts[i].Id])
}
} else {
assert.Equal(t, expectedPosts, actualPosts)
}
// Check common fields between threads and posts.
for _, thread := range threads {
assert.Equal(t, thread.DeleteAt, thread.Post.DeleteAt, "expected Thread.DeleteAt == Post.DeleteAt")
}
}
t.Run("GetThreadsForUser", func(t *testing.T) { t.Run("GetThreadsForUser", func(t *testing.T) {
testCases := []struct { testCases := []struct {
Description string Description string
@@ -1005,19 +1077,7 @@ func testVarious(t *testing.T, ss store.Store) {
threads, err := ss.Thread().GetThreadsForUser(testCase.UserID, testCase.TeamID, testCase.Options) threads, err := ss.Thread().GetThreadsForUser(testCase.UserID, testCase.TeamID, testCase.Options)
require.NoError(t, err) require.NoError(t, err)
postIDs := make([]string, 0, len(threads)) assertThreadPosts(t, threads, testCase.ExpectedThreads)
for _, thread := range threads {
postIDs = append(postIDs, thread.PostId)
}
sort.Strings(postIDs)
expectedPostIDs := make([]string, 0, len(testCase.ExpectedThreads))
for _, post := range testCase.ExpectedThreads {
expectedPostIDs = append(expectedPostIDs, post.Id)
}
sort.Strings(expectedPostIDs)
assert.Equal(t, expectedPostIDs, postIDs)
}) })
} }
}) })

Просмотреть файл

@@ -244,6 +244,9 @@ func testUserStoreUpdateUpdateAt(t *testing.T, ss store.Store) {
_, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1) _, nErr := ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr) require.NoError(t, nErr)
// Ensure UpdateAt has a change to be different below.
time.Sleep(1 * time.Millisecond)
_, err = ss.User().UpdateUpdateAt(u1.Id) _, err = ss.User().UpdateUpdateAt(u1.Id)
require.NoError(t, err) require.NoError(t, err)