MM-63725 Populate multiple sidebar categories at once whenever possible (#31064)

* Remove redundant sidebar tests from TestChannelStore

* MM-63725 Refactor to split out getOrphanedSidebarChannels

* MM-63725 Populate multiple sidebar categories at once whenever possible

* Fix shadowing
Этот коммит содержится в:
Harrison Healey
2025-06-19 10:09:36 -04:00
коммит произвёл GitHub
родитель 3bd0a2ad91
Коммит a3f60f797b
2 изменённых файлов: 90 добавлений и 39 удалений

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

@@ -414,25 +414,88 @@ func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategor
return result, nil return result, nil
} }
func (s SqlChannelStore) completePopulatingCategoryChannelsT(db sqlxExecutor, category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) { // completePopulatingCategoryT ensures that any orphaned channels are always included in a sidebar category by adding
if category.Type == model.SidebarCategoryCustom || category.Type == model.SidebarCategoryFavorites { // orphaned channels to either the Channels category or DMs category. It has no effect on other types of categories.
return category, nil func (s SqlChannelStore) completePopulatingCategoryT(db sqlxExecutor, category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) {
populatedChannels, err := s.getOrphanedSidebarChannels(
db,
category.UserId,
category.TeamId,
category.Type == model.SidebarCategoryChannels,
category.Type == model.SidebarCategoryDirectMessages,
)
if err != nil {
return nil, errors.Wrap(err, "Failed to get orphaned sidebar channels")
}
for _, channel := range populatedChannels {
category.Channels = append(category.Channels, channel.Id)
}
return category, nil
}
// completePopulatingCategoriesT ensures that any orphaned channels are always included in a sidebar category by adding
// orphaned channels to either the Channels category or DMs category. It has no effect on other types of categories.
func (s SqlChannelStore) completePopulatingCategoriesT(db sqlxExecutor, userId string, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) {
// Find the channels and DMs categories to know what to get from the database
channelsIndex := -1
dmsIndex := -1
for i, category := range categories {
if category.Type == model.SidebarCategoryChannels {
channelsIndex = i
} else if category.Type == model.SidebarCategoryDirectMessages {
dmsIndex = i
}
}
populatedChannels, err := s.getOrphanedSidebarChannels(
db,
userId,
teamId,
channelsIndex != -1,
dmsIndex != -1,
)
if err != nil {
return nil, errors.Wrap(err, "Failed to get orphaned sidebar channels")
}
// Sort the returned channels into their corresponding category
for _, channel := range populatedChannels {
if channelsIndex != -1 && (channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypePrivate) {
categories[channelsIndex].Channels = append(categories[channelsIndex].Channels, channel.Id)
} else if dmsIndex != -1 && (channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup) {
categories[dmsIndex].Channels = append(categories[dmsIndex].Channels, channel.Id)
}
}
return categories, nil
}
type OrphanedSidebarChannel struct {
Id string
Type model.ChannelType
}
// getOrphanedSidebarChannels returns all of the user's channels on a given team that aren't explicitly in any category.
func (s SqlChannelStore) getOrphanedSidebarChannels(db sqlxExecutor, userId string, teamId string, selectChannels bool, selectDMs bool) ([]*OrphanedSidebarChannel, error) {
if !selectChannels && !selectDMs {
return nil, nil
} }
isMySQL := s.DriverName() == model.DatabaseDriverMysql isMySQL := s.DriverName() == model.DatabaseDriverMysql
var channelTypeFilter sq.Sqlizer channelTypeFilter := sq.Or{}
if category.Type == model.SidebarCategoryDirectMessages { if selectDMs {
// any DM/GM channels that aren't in any category should be returned as part of the Direct Messages category // any DM/GM channels that aren't in any category should be returned as part of the Direct Messages category
channelTypeFilter = sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}} channelTypeFilter = append(channelTypeFilter, sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}})
} else if category.Type == model.SidebarCategoryChannels { }
if selectChannels {
// any public/private channels that are on the current team and aren't in any category should be returned as part of the Channels category // any public/private channels that are on the current team and aren't in any category should be returned as part of the Channels category
channelTypeFilter = sq.And{ channelTypeFilter = append(channelTypeFilter, sq.And{
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeOpen, model.ChannelTypePrivate}}, sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeOpen, model.ChannelTypePrivate}},
sq.Eq{"Channels.TeamId": category.TeamId}, sq.Eq{"Channels.TeamId": teamId},
} })
} else {
return nil, fmt.Errorf("invalid category type: %q", category.Type)
} }
// A subquery that is true if the channel does not have a SidebarChannel entry for the current user on the current team // A subquery that is true if the channel does not have a SidebarChannel entry for the current user on the current team
@@ -454,26 +517,26 @@ func (s SqlChannelStore) completePopulatingCategoryChannelsT(db sqlxExecutor, ca
doesNotHaveSidebarChannel = doesNotHaveSidebarChannel.Where(sq.And{ doesNotHaveSidebarChannel = doesNotHaveSidebarChannel.Where(sq.And{
sq.Expr("SidebarChannels.ChannelId = ChannelMembers.ChannelId"), sq.Expr("SidebarChannels.ChannelId = ChannelMembers.ChannelId"),
sq.Eq{"SidebarCategories.UserId": category.UserId}, sq.Eq{"SidebarCategories.UserId": userId},
sq.Eq{"SidebarCategories.TeamId": category.TeamId}, sq.Eq{"SidebarCategories.TeamId": teamId},
}) })
channels := []string{} channels := []*OrphanedSidebarChannel{}
var col string var col string
if isMySQL { if isMySQL {
// This is a materialization hint for MySQL to materialize // This is a materialization hint for MySQL to materialize
// the doesNotHaveSidebarChannel sub-query // the doesNotHaveSidebarChannel sub-query
// Without this hint, MySQL is unable to come up with this plan by itself. // Without this hint, MySQL is unable to come up with this plan by itself.
col = "/*+ SEMIJOIN(@subq1 MATERIALIZATION) */ Id" col = "/*+ SEMIJOIN(@subq1 MATERIALIZATION) */ Id, Channels.Type"
} else { } else {
col = "Id" col = "Id, Channels.Type"
} }
sql, args, err := s.getQueryBuilder(). sql, args, err := s.getQueryBuilder().
Select(col). Select(col).
From("ChannelMembers"). From("ChannelMembers").
LeftJoin("Channels ON Channels.Id=ChannelMembers.ChannelId"). LeftJoin("Channels ON Channels.Id=ChannelMembers.ChannelId").
Where(sq.And{ Where(sq.And{
sq.Eq{"ChannelMembers.UserId": category.UserId}, sq.Eq{"ChannelMembers.UserId": userId},
channelTypeFilter, channelTypeFilter,
sq.Eq{"Channels.DeleteAt": 0}, sq.Eq{"Channels.DeleteAt": 0},
doesNotHaveSidebarChannel, doesNotHaveSidebarChannel,
@@ -487,8 +550,7 @@ func (s SqlChannelStore) completePopulatingCategoryChannelsT(db sqlxExecutor, ca
return nil, errors.Wrap(err, "failed to get channel members") return nil, errors.Wrap(err, "failed to get channel members")
} }
category.Channels = append(channels, category.Channels...) return channels, nil
return category, nil
} }
func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, error) { func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, error) {
@@ -525,7 +587,7 @@ func (s SqlChannelStore) getSidebarCategoryT(db sqlxExecutor, categoryId string)
result.Channels = append(result.Channels, *category.ChannelId) result.Channels = append(result.Channels, *category.ChannelId)
} }
} }
return s.completePopulatingCategoryChannelsT(db, result) return s.completePopulatingCategoryT(db, result)
} }
func (s SqlChannelStore) getSidebarCategoriesT(db sqlxExecutor, userId string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) { func (s SqlChannelStore) getSidebarCategoriesT(db sqlxExecutor, userId string, opts *store.SidebarCategorySearchOpts) (*model.OrderedSidebarCategories, error) {
@@ -589,10 +651,9 @@ func (s SqlChannelStore) getSidebarCategoriesT(db sqlxExecutor, userId string, o
prevCategory.Channels = append(prevCategory.Channels, *category.ChannelId) prevCategory.Channels = append(prevCategory.Channels, *category.ChannelId)
} }
} }
for _, category := range oc.Categories {
if _, err := s.completePopulatingCategoryChannelsT(db, category); err != nil { if _, err := s.completePopulatingCategoriesT(db, userId, opts.TeamID, oc.Categories); err != nil {
return nil, err return nil, err
}
} }
return &oc, nil return &oc, nil
@@ -731,6 +792,8 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori
copy(destCategory.Channels, category.Channels) copy(destCategory.Channels, category.Channels)
destCategory.Muted = category.Muted destCategory.Muted = category.Muted
} else {
destCategory.Channels = make([]string, 0)
} }
updatedCategories = append(updatedCategories, destCategory) updatedCategories = append(updatedCategories, destCategory)
@@ -877,13 +940,8 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori
} }
// Ensure Channels are populated for Channels/Direct Messages category if they change // Ensure Channels are populated for Channels/Direct Messages category if they change
for i, updatedCategory := range updatedCategories { if _, nErr := s.completePopulatingCategoriesT(transaction, userId, teamId, updatedCategories); nErr != nil {
populated, nErr := s.completePopulatingCategoryChannelsT(transaction, updatedCategory) return nil, nil, nErr
if nErr != nil {
return nil, nil, nErr
}
updatedCategories[i] = populated
} }
if err = transaction.Commit(); err != nil { if err = transaction.Commit(); err != nil {

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

@@ -147,13 +147,6 @@ func TestChannelStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore
t.Run("ExportAllDirectChannelsDeletedChannel", func(t *testing.T) { testChannelStoreExportAllDirectChannelsDeletedChannel(t, rctx, ss, s) }) t.Run("ExportAllDirectChannelsDeletedChannel", func(t *testing.T) { testChannelStoreExportAllDirectChannelsDeletedChannel(t, rctx, ss, s) })
t.Run("GetChannelsBatchForIndexing", func(t *testing.T) { testChannelStoreGetChannelsBatchForIndexing(t, rctx, ss) }) t.Run("GetChannelsBatchForIndexing", func(t *testing.T) { testChannelStoreGetChannelsBatchForIndexing(t, rctx, ss) })
t.Run("GroupSyncedChannelCount", func(t *testing.T) { testGroupSyncedChannelCount(t, rctx, ss) }) t.Run("GroupSyncedChannelCount", func(t *testing.T) { testGroupSyncedChannelCount(t, rctx, ss) })
t.Run("CreateInitialSidebarCategories", func(t *testing.T) { testCreateInitialSidebarCategories(t, rctx, ss) })
t.Run("CreateSidebarCategory", func(t *testing.T) { testCreateSidebarCategory(t, rctx, ss) })
t.Run("GetSidebarCategory", func(t *testing.T) { testGetSidebarCategory(t, rctx, ss, s) })
t.Run("GetSidebarCategories", func(t *testing.T) { testGetSidebarCategories(t, rctx, ss) })
t.Run("UpdateSidebarCategories", func(t *testing.T) { testUpdateSidebarCategories(t, rctx, ss) })
t.Run("DeleteSidebarCategory", func(t *testing.T) { testDeleteSidebarCategory(t, rctx, ss, s) })
t.Run("UpdateSidebarChannelsByPreferences", func(t *testing.T) { testUpdateSidebarChannelsByPreferences(t, rctx, ss) })
t.Run("SetShared", func(t *testing.T) { testSetShared(t, rctx, ss) }) t.Run("SetShared", func(t *testing.T) { testSetShared(t, rctx, ss) })
t.Run("GetTeamForChannel", func(t *testing.T) { testGetTeamForChannel(t, rctx, ss) }) t.Run("GetTeamForChannel", func(t *testing.T) { testGetTeamForChannel(t, rctx, ss) })
t.Run("GetChannelsWithUnreadsAndWithMentions", func(t *testing.T) { testGetChannelsWithUnreadsAndWithMentions(t, rctx, ss) }) t.Run("GetChannelsWithUnreadsAndWithMentions", func(t *testing.T) { testGetChannelsWithUnreadsAndWithMentions(t, rctx, ss) })