MM-43045: minimize JOIN Posts (#19934)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
348602cf00
Коммит
5bd223c836
@@ -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("Update", func(t *testing.T) { testPostStoreUpdate(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("PermDelete1Level2", func(t *testing.T) { testPostStorePermDelete1Level2(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.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})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -819,109 +820,132 @@ func testPostStoreUpdate(t *testing.T, ss store.Store) {
|
||||
}
|
||||
|
||||
func testPostStoreDelete(t *testing.T, ss store.Store) {
|
||||
o1 := &model.Post{}
|
||||
o1.ChannelId = model.NewId()
|
||||
o1.UserId = model.NewId()
|
||||
o1.Message = model.NewRandomString(10)
|
||||
deleteByID := model.NewId()
|
||||
t.Run("single post, no replies", func(t *testing.T) {
|
||||
// Create a post
|
||||
rootPost, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
Message: model.NewRandomString(10),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
etag1 := ss.Post().GetEtag(o1.ChannelId, false, false)
|
||||
require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag")
|
||||
// Verify etag generation for the channel containing the post.
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
// Verify the created post.
|
||||
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{}, "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
|
||||
// Mark the post as deleted by the user identified with deleteByID.
|
||||
deleteByID := model.NewId()
|
||||
err = ss.Post().Delete(rootPost.Id, model.GetMillis(), deleteByID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ss.Post().Delete(o1.Id, model.GetMillis(), deleteByID)
|
||||
require.NoError(t, err)
|
||||
// Ensure the appropriate posts prop reflects the user deleting the post.
|
||||
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)
|
||||
post := posts[0]
|
||||
actual := post.GetProp(model.PostPropsDeleteBy)
|
||||
// Verify that the post is no longer fetched by default.
|
||||
_, err = ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "")
|
||||
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{}, "")
|
||||
require.Error(t, err, "Missing id should have failed - PostList %v", r3)
|
||||
t.Run("thread with one reply", func(t *testing.T) {
|
||||
// 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)
|
||||
require.Equal(t, 0, strings.Index(etag2, model.CurrentVersion+"."), "Invalid Etag")
|
||||
}
|
||||
// Reply to that root post
|
||||
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) {
|
||||
o1 := &model.Post{}
|
||||
o1.ChannelId = model.NewId()
|
||||
o1.UserId = model.NewId()
|
||||
o1.Message = NewTestId()
|
||||
o1, err := ss.Post().Save(o1)
|
||||
require.NoError(t, err)
|
||||
// Delete the root post
|
||||
err = ss.Post().Delete(rootPost.Id, model.GetMillis(), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
o2 := &model.Post{}
|
||||
o2.ChannelId = o1.ChannelId
|
||||
o2.UserId = model.NewId()
|
||||
o2.Message = NewTestId()
|
||||
o2.RootId = o1.Id
|
||||
o2, err = ss.Post().Save(o2)
|
||||
require.NoError(t, err)
|
||||
// Verify the root post deleted
|
||||
_, err = ss.Post().Get(context.Background(), rootPost.Id, model.GetPostsOptions{}, "")
|
||||
require.Error(t, err, "Deleted id should have failed")
|
||||
require.IsType(t, &store.ErrNotFound{}, err)
|
||||
|
||||
err = ss.Post().Delete(o1.Id, model.GetMillis(), "")
|
||||
require.NoError(t, err)
|
||||
// Verify the reply post deleted
|
||||
_, 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{}, "")
|
||||
require.Error(t, err, "Deleted id should have failed")
|
||||
t.Run("thread with multiple replies", func(t *testing.T) {
|
||||
// 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{}, "")
|
||||
require.Error(t, err, "Deleted id should have failed")
|
||||
}
|
||||
// Reply to that root post
|
||||
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) {
|
||||
o1 := &model.Post{}
|
||||
o1.ChannelId = model.NewId()
|
||||
o1.UserId = model.NewId()
|
||||
o1.Message = NewTestId()
|
||||
o1, err := ss.Post().Save(o1)
|
||||
require.NoError(t, err)
|
||||
// Reply to that root post a second time
|
||||
replyPost2, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: rootPost1.ChannelId,
|
||||
UserId: model.NewId(),
|
||||
Message: NewTestId(),
|
||||
RootId: rootPost1.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
o2 := &model.Post{}
|
||||
o2.ChannelId = o1.ChannelId
|
||||
o2.UserId = model.NewId()
|
||||
o2.Message = NewTestId()
|
||||
o2.RootId = o1.Id
|
||||
o2, err = ss.Post().Save(o2)
|
||||
require.NoError(t, err)
|
||||
// Create another root post in a separate channel
|
||||
rootPost2, err := ss.Post().Save(&model.Post{
|
||||
ChannelId: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
Message: NewTestId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
o3 := &model.Post{}
|
||||
o3.ChannelId = o1.ChannelId
|
||||
o3.UserId = model.NewId()
|
||||
o3.Message = NewTestId()
|
||||
o3.RootId = o1.Id
|
||||
o3, err = ss.Post().Save(o3)
|
||||
require.NoError(t, err)
|
||||
// Delete the root post
|
||||
err = ss.Post().Delete(rootPost1.Id, model.GetMillis(), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
o4 := &model.Post{}
|
||||
o4.ChannelId = model.NewId()
|
||||
o4.UserId = model.NewId()
|
||||
o4.Message = NewTestId()
|
||||
o4, err = ss.Post().Save(o4)
|
||||
require.NoError(t, err)
|
||||
// Verify the root post and replies deleted
|
||||
_, err = ss.Post().Get(context.Background(), rootPost1.Id, model.GetPostsOptions{}, "")
|
||||
require.Error(t, err, "Deleted id should have failed")
|
||||
|
||||
err = ss.Post().Delete(o1.Id, model.GetMillis(), "")
|
||||
require.NoError(t, err)
|
||||
_, err = ss.Post().Get(context.Background(), replyPost1.Id, model.GetPostsOptions{}, "")
|
||||
require.Error(t, err, "Deleted id should have failed")
|
||||
|
||||
_, err = ss.Post().Get(context.Background(), o1.Id, model.GetPostsOptions{}, "")
|
||||
require.Error(t, err, "Deleted id should have failed")
|
||||
_, err = ss.Post().Get(context.Background(), replyPost2.Id, model.GetPostsOptions{}, "")
|
||||
require.Error(t, err, "Deleted id should have failed")
|
||||
|
||||
_, err = ss.Post().Get(context.Background(), o2.Id, model.GetPostsOptions{}, "")
|
||||
require.Error(t, err, "Deleted id should have failed")
|
||||
|
||||
_, 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)
|
||||
// Verify other root posts remain undeleted.
|
||||
_, err = ss.Post().Get(context.Background(), rootPost2.Id, model.GetPostsOptions{}, "")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
createThreadMembership := func(userID, postID string, isMention bool) {
|
||||
t.Helper()
|
||||
@@ -818,6 +824,16 @@ func testVarious(t *testing.T, ss store.Store) {
|
||||
})
|
||||
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, team1channel1post2.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)
|
||||
threadStoreCreateReply(t, ss, team1channel1.Id, team1channel1post2.Id, user2ID, model.GetMillis())
|
||||
|
||||
// Actually make team2channel1post2deleted deleted
|
||||
err = ss.Post().Delete(team2channel1post2deleted.Id, model.GetMillis(), user1ID)
|
||||
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) {
|
||||
testCases := []struct {
|
||||
Description string
|
||||
@@ -899,27 +943,15 @@ func testVarious(t *testing.T, ss store.Store) {
|
||||
{"team1, user1", user1ID, team1.Id, model.GetUserThreadsOpts{}, []*model.Post{
|
||||
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{
|
||||
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{
|
||||
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{
|
||||
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 {
|
||||
@@ -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) {
|
||||
testCases := []struct {
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
postIDs := make([]string, 0, len(threads))
|
||||
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)
|
||||
assertThreadPosts(t, threads, testCase.ExpectedThreads)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
require.NoError(t, nErr)
|
||||
|
||||
// Ensure UpdateAt has a change to be different below.
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
_, err = ss.User().UpdateUpdateAt(u1.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user