Files
mostlymatter/store/sqlstore/channel_store.go
Ashish Bhate e8d431ee7c [MM-26837] attempt at fixing flaky test on mysql-5.6 (#15110)
Summary

- Attempt at fixing the AutocompleteInTeam/empty_string test which is flaky on MySQL-5.6. This is likely an index_merge_intersection optimization issue. See https://community.mattermost.com/core/pl/uzgygwa9e3d4pns14ayfs7mobw
    for more discussion. The fix here simply modifies the where clause to change the query plan so index_merge_intersection is not used, while also not introducing a temporary table or a filesort.

Ticket Link

- https://mattermost.atlassian.net/browse/MM-26837
2020-08-13 20:03:26 +05:30

4289 строки
155 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/mattermost/gorp"
"github.com/mattermost/mattermost-server/v5/einterfaces"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/services/cache"
"github.com/mattermost/mattermost-server/v5/store"
sq "github.com/Masterminds/squirrel"
"github.com/pkg/errors"
)
const (
ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE = model.SESSION_CACHE_SIZE
ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_DURATION = 15 * time.Minute // 15 mins
ALL_CHANNEL_MEMBERS_NOTIFY_PROPS_FOR_CHANNEL_CACHE_SIZE = model.SESSION_CACHE_SIZE
ALL_CHANNEL_MEMBERS_NOTIFY_PROPS_FOR_CHANNEL_CACHE_DURATION = 30 * time.Minute // 30 mins
CHANNEL_CACHE_DURATION = 15 * time.Minute // 15 mins
)
type SqlChannelStore struct {
SqlStore
metrics einterfaces.MetricsInterface
}
type channelMember struct {
ChannelId string
UserId string
Roles string
LastViewedAt int64
MsgCount int64
MentionCount int64
NotifyProps model.StringMap
LastUpdateAt int64
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
SchemeGuest sql.NullBool
}
func NewChannelMemberFromModel(cm *model.ChannelMember) *channelMember {
return &channelMember{
ChannelId: cm.ChannelId,
UserId: cm.UserId,
Roles: cm.ExplicitRoles,
LastViewedAt: cm.LastViewedAt,
MsgCount: cm.MsgCount,
MentionCount: cm.MentionCount,
NotifyProps: cm.NotifyProps,
LastUpdateAt: cm.LastUpdateAt,
SchemeGuest: sql.NullBool{Valid: true, Bool: cm.SchemeGuest},
SchemeUser: sql.NullBool{Valid: true, Bool: cm.SchemeUser},
SchemeAdmin: sql.NullBool{Valid: true, Bool: cm.SchemeAdmin},
}
}
type channelMemberWithSchemeRoles struct {
ChannelId string
UserId string
Roles string
LastViewedAt int64
MsgCount int64
MentionCount int64
NotifyProps model.StringMap
LastUpdateAt int64
SchemeGuest sql.NullBool
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
TeamSchemeDefaultGuestRole sql.NullString
TeamSchemeDefaultUserRole sql.NullString
TeamSchemeDefaultAdminRole sql.NullString
ChannelSchemeDefaultGuestRole sql.NullString
ChannelSchemeDefaultUserRole sql.NullString
ChannelSchemeDefaultAdminRole sql.NullString
}
func channelMemberSliceColumns() []string {
return []string{"ChannelId", "UserId", "Roles", "LastViewedAt", "MsgCount", "MentionCount", "NotifyProps", "LastUpdateAt", "SchemeUser", "SchemeAdmin", "SchemeGuest"}
}
func channelMemberToSlice(member *model.ChannelMember) []interface{} {
resultSlice := []interface{}{}
resultSlice = append(resultSlice, member.ChannelId)
resultSlice = append(resultSlice, member.UserId)
resultSlice = append(resultSlice, member.ExplicitRoles)
resultSlice = append(resultSlice, member.LastViewedAt)
resultSlice = append(resultSlice, member.MsgCount)
resultSlice = append(resultSlice, member.MentionCount)
resultSlice = append(resultSlice, model.MapToJson(member.NotifyProps))
resultSlice = append(resultSlice, member.LastUpdateAt)
resultSlice = append(resultSlice, member.SchemeUser)
resultSlice = append(resultSlice, member.SchemeAdmin)
resultSlice = append(resultSlice, member.SchemeGuest)
return resultSlice
}
type channelMemberWithSchemeRolesList []channelMemberWithSchemeRoles
func getChannelRoles(schemeGuest, schemeUser, schemeAdmin bool, defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole, defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole string, roles []string) rolesInfo {
result := rolesInfo{
roles: []string{},
explicitRoles: []string{},
schemeGuest: schemeGuest,
schemeUser: schemeUser,
schemeAdmin: schemeAdmin,
}
// Identify any scheme derived roles that are in "Roles" field due to not yet being migrated, and exclude
// them from ExplicitRoles field.
for _, role := range roles {
switch role {
case model.CHANNEL_GUEST_ROLE_ID:
result.schemeGuest = true
case model.CHANNEL_USER_ROLE_ID:
result.schemeUser = true
case model.CHANNEL_ADMIN_ROLE_ID:
result.schemeAdmin = true
default:
result.explicitRoles = append(result.explicitRoles, role)
result.roles = append(result.roles, role)
}
}
// Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
// them to the Roles field for backwards compatibility reasons.
var schemeImpliedRoles []string
if result.schemeGuest {
if defaultChannelGuestRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultChannelGuestRole)
} else if defaultTeamGuestRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamGuestRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_GUEST_ROLE_ID)
}
}
if result.schemeUser {
if defaultChannelUserRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultChannelUserRole)
} else if defaultTeamUserRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamUserRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_USER_ROLE_ID)
}
}
if result.schemeAdmin {
if defaultChannelAdminRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultChannelAdminRole)
} else if defaultTeamAdminRole != "" {
schemeImpliedRoles = append(schemeImpliedRoles, defaultTeamAdminRole)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_ADMIN_ROLE_ID)
}
}
for _, impliedRole := range schemeImpliedRoles {
alreadyThere := false
for _, role := range result.roles {
if role == impliedRole {
alreadyThere = true
break
}
}
if !alreadyThere {
result.roles = append(result.roles, impliedRole)
}
}
return result
}
func (db channelMemberWithSchemeRoles) ToModel() *model.ChannelMember {
// Identify any system-wide scheme derived roles that are in "Roles" field due to not yet being migrated,
// and exclude them from ExplicitRoles field.
schemeGuest := db.SchemeGuest.Valid && db.SchemeGuest.Bool
schemeUser := db.SchemeUser.Valid && db.SchemeUser.Bool
schemeAdmin := db.SchemeAdmin.Valid && db.SchemeAdmin.Bool
defaultTeamGuestRole := ""
if db.TeamSchemeDefaultGuestRole.Valid {
defaultTeamGuestRole = db.TeamSchemeDefaultGuestRole.String
}
defaultTeamUserRole := ""
if db.TeamSchemeDefaultUserRole.Valid {
defaultTeamUserRole = db.TeamSchemeDefaultUserRole.String
}
defaultTeamAdminRole := ""
if db.TeamSchemeDefaultAdminRole.Valid {
defaultTeamAdminRole = db.TeamSchemeDefaultAdminRole.String
}
defaultChannelGuestRole := ""
if db.ChannelSchemeDefaultGuestRole.Valid {
defaultChannelGuestRole = db.ChannelSchemeDefaultGuestRole.String
}
defaultChannelUserRole := ""
if db.ChannelSchemeDefaultUserRole.Valid {
defaultChannelUserRole = db.ChannelSchemeDefaultUserRole.String
}
defaultChannelAdminRole := ""
if db.ChannelSchemeDefaultAdminRole.Valid {
defaultChannelAdminRole = db.ChannelSchemeDefaultAdminRole.String
}
rolesResult := getChannelRoles(
schemeGuest, schemeUser, schemeAdmin,
defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole,
defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole,
strings.Fields(db.Roles),
)
return &model.ChannelMember{
ChannelId: db.ChannelId,
UserId: db.UserId,
Roles: strings.Join(rolesResult.roles, " "),
LastViewedAt: db.LastViewedAt,
MsgCount: db.MsgCount,
MentionCount: db.MentionCount,
NotifyProps: db.NotifyProps,
LastUpdateAt: db.LastUpdateAt,
SchemeAdmin: rolesResult.schemeAdmin,
SchemeUser: rolesResult.schemeUser,
SchemeGuest: rolesResult.schemeGuest,
ExplicitRoles: strings.Join(rolesResult.explicitRoles, " "),
}
}
func (db channelMemberWithSchemeRolesList) ToModel() *model.ChannelMembers {
cms := model.ChannelMembers{}
for _, cm := range db {
cms = append(cms, *cm.ToModel())
}
return &cms
}
type allChannelMember struct {
ChannelId string
Roles string
SchemeGuest sql.NullBool
SchemeUser sql.NullBool
SchemeAdmin sql.NullBool
TeamSchemeDefaultGuestRole sql.NullString
TeamSchemeDefaultUserRole sql.NullString
TeamSchemeDefaultAdminRole sql.NullString
ChannelSchemeDefaultGuestRole sql.NullString
ChannelSchemeDefaultUserRole sql.NullString
ChannelSchemeDefaultAdminRole sql.NullString
}
type allChannelMembers []allChannelMember
func (db allChannelMember) Process() (string, string) {
roles := strings.Fields(db.Roles)
// Add any scheme derived roles that are not in the Roles field due to being Implicit from the Scheme, and add
// them to the Roles field for backwards compatibility reasons.
var schemeImpliedRoles []string
if db.SchemeGuest.Valid && db.SchemeGuest.Bool {
if db.ChannelSchemeDefaultGuestRole.Valid && db.ChannelSchemeDefaultGuestRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultGuestRole.String)
} else if db.TeamSchemeDefaultGuestRole.Valid && db.TeamSchemeDefaultGuestRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultGuestRole.String)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_GUEST_ROLE_ID)
}
}
if db.SchemeUser.Valid && db.SchemeUser.Bool {
if db.ChannelSchemeDefaultUserRole.Valid && db.ChannelSchemeDefaultUserRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultUserRole.String)
} else if db.TeamSchemeDefaultUserRole.Valid && db.TeamSchemeDefaultUserRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultUserRole.String)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_USER_ROLE_ID)
}
}
if db.SchemeAdmin.Valid && db.SchemeAdmin.Bool {
if db.ChannelSchemeDefaultAdminRole.Valid && db.ChannelSchemeDefaultAdminRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.ChannelSchemeDefaultAdminRole.String)
} else if db.TeamSchemeDefaultAdminRole.Valid && db.TeamSchemeDefaultAdminRole.String != "" {
schemeImpliedRoles = append(schemeImpliedRoles, db.TeamSchemeDefaultAdminRole.String)
} else {
schemeImpliedRoles = append(schemeImpliedRoles, model.CHANNEL_ADMIN_ROLE_ID)
}
}
for _, impliedRole := range schemeImpliedRoles {
alreadyThere := false
for _, role := range roles {
if role == impliedRole {
alreadyThere = true
}
}
if !alreadyThere {
roles = append(roles, impliedRole)
}
}
return db.ChannelId, strings.Join(roles, " ")
}
func (db allChannelMembers) ToMapStringString() map[string]string {
result := make(map[string]string)
for _, item := range db {
key, value := item.Process()
result[key] = value
}
return result
}
// publicChannel is a subset of the metadata corresponding to public channels only.
type publicChannel struct {
Id string `json:"id"`
DeleteAt int64 `json:"delete_at"`
TeamId string `json:"team_id"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
}
var allChannelMembersForUserCache = cache.NewLRU(&cache.LRUOptions{
Size: ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE,
})
var allChannelMembersNotifyPropsForChannelCache = cache.NewLRU(&cache.LRUOptions{
Size: ALL_CHANNEL_MEMBERS_NOTIFY_PROPS_FOR_CHANNEL_CACHE_SIZE,
})
var channelByNameCache = cache.NewLRU(&cache.LRUOptions{
Size: model.CHANNEL_CACHE_SIZE,
})
func (s SqlChannelStore) ClearCaches() {
allChannelMembersForUserCache.Purge()
allChannelMembersNotifyPropsForChannelCache.Purge()
channelByNameCache.Purge()
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Purge")
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Purge")
s.metrics.IncrementMemCacheInvalidationCounter("Channel By Name - Purge")
}
}
func newSqlChannelStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.ChannelStore {
s := &SqlChannelStore{
SqlStore: sqlStore,
metrics: metrics,
}
for _, db := range sqlStore.GetAllConns() {
table := db.AddTableWithName(model.Channel{}, "Channels").SetKeys(false, "Id")
table.ColMap("Id").SetMaxSize(26)
table.ColMap("TeamId").SetMaxSize(26)
table.ColMap("Type").SetMaxSize(1)
table.ColMap("DisplayName").SetMaxSize(64)
table.ColMap("Name").SetMaxSize(64)
table.SetUniqueTogether("Name", "TeamId")
table.ColMap("Header").SetMaxSize(1024)
table.ColMap("Purpose").SetMaxSize(250)
table.ColMap("CreatorId").SetMaxSize(26)
table.ColMap("SchemeId").SetMaxSize(26)
tablem := db.AddTableWithName(channelMember{}, "ChannelMembers").SetKeys(false, "ChannelId", "UserId")
tablem.ColMap("ChannelId").SetMaxSize(26)
tablem.ColMap("UserId").SetMaxSize(26)
tablem.ColMap("Roles").SetMaxSize(64)
tablem.ColMap("NotifyProps").SetMaxSize(2000)
tablePublicChannels := db.AddTableWithName(publicChannel{}, "PublicChannels").SetKeys(false, "Id")
tablePublicChannels.ColMap("Id").SetMaxSize(26)
tablePublicChannels.ColMap("TeamId").SetMaxSize(26)
tablePublicChannels.ColMap("DisplayName").SetMaxSize(64)
tablePublicChannels.ColMap("Name").SetMaxSize(64)
tablePublicChannels.SetUniqueTogether("Name", "TeamId")
tablePublicChannels.ColMap("Header").SetMaxSize(1024)
tablePublicChannels.ColMap("Purpose").SetMaxSize(250)
tableSidebarCategories := db.AddTableWithName(model.SidebarCategory{}, "SidebarCategories").SetKeys(false, "Id")
tableSidebarCategories.ColMap("Id").SetMaxSize(26)
tableSidebarCategories.ColMap("UserId").SetMaxSize(26)
tableSidebarCategories.ColMap("TeamId").SetMaxSize(26)
tableSidebarCategories.ColMap("Sorting").SetMaxSize(64)
tableSidebarCategories.ColMap("Type").SetMaxSize(64)
tableSidebarCategories.ColMap("DisplayName").SetMaxSize(64)
tableSidebarChannels := db.AddTableWithName(model.SidebarChannel{}, "SidebarChannels").SetKeys(false, "ChannelId", "UserId", "CategoryId")
tableSidebarChannels.ColMap("ChannelId").SetMaxSize(26)
tableSidebarChannels.ColMap("UserId").SetMaxSize(26)
tableSidebarChannels.ColMap("CategoryId").SetMaxSize(26)
}
return s
}
func (s SqlChannelStore) createIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_channels_team_id", "Channels", "TeamId")
s.CreateIndexIfNotExists("idx_channels_name", "Channels", "Name")
s.CreateIndexIfNotExists("idx_channels_update_at", "Channels", "UpdateAt")
s.CreateIndexIfNotExists("idx_channels_create_at", "Channels", "CreateAt")
s.CreateIndexIfNotExists("idx_channels_delete_at", "Channels", "DeleteAt")
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
s.CreateIndexIfNotExists("idx_channels_name_lower", "Channels", "lower(Name)")
s.CreateIndexIfNotExists("idx_channels_displayname_lower", "Channels", "lower(DisplayName)")
}
s.CreateIndexIfNotExists("idx_channelmembers_channel_id", "ChannelMembers", "ChannelId")
s.CreateIndexIfNotExists("idx_channelmembers_user_id", "ChannelMembers", "UserId")
s.CreateFullTextIndexIfNotExists("idx_channel_search_txt", "Channels", "Name, DisplayName, Purpose")
s.CreateIndexIfNotExists("idx_publicchannels_team_id", "PublicChannels", "TeamId")
s.CreateIndexIfNotExists("idx_publicchannels_name", "PublicChannels", "Name")
s.CreateIndexIfNotExists("idx_publicchannels_delete_at", "PublicChannels", "DeleteAt")
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
s.CreateIndexIfNotExists("idx_publicchannels_name_lower", "PublicChannels", "lower(Name)")
s.CreateIndexIfNotExists("idx_publicchannels_displayname_lower", "PublicChannels", "lower(DisplayName)")
}
s.CreateFullTextIndexIfNotExists("idx_publicchannels_search_txt", "PublicChannels", "Name, DisplayName, Purpose")
s.CreateIndexIfNotExists("idx_channels_scheme_id", "Channels", "SchemeId")
}
func (s SqlChannelStore) CreateInitialSidebarCategories(userId, teamId string) error {
transaction, err := s.GetMaster().Begin()
if err != nil {
return errors.Wrap(err, "CreateInitialSidebarCategories: begin_transaction")
}
defer finalizeTransaction(transaction)
if err := s.createInitialSidebarCategoriesT(transaction, userId, teamId); err != nil {
return errors.Wrap(err, "CreateInitialSidebarCategories: createInitialSidebarCategoriesT")
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "CreateInitialSidebarCategories: commit_transaction")
}
return nil
}
func (s SqlChannelStore) createInitialSidebarCategoriesT(transaction *gorp.Transaction, userId, teamId string) error {
selectQuery, selectParams, _ := s.getQueryBuilder().
Select("Type").
From("SidebarCategories").
Where(sq.Eq{
"UserId": userId,
"TeamId": teamId,
"Type": []model.SidebarCategoryType{model.SidebarCategoryFavorites, model.SidebarCategoryChannels, model.SidebarCategoryDirectMessages},
}).ToSql()
var existingTypes []model.SidebarCategoryType
_, err := transaction.Select(&existingTypes, selectQuery, selectParams...)
if err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to select existing categories")
}
hasCategoryOfType := make(map[model.SidebarCategoryType]bool, len(existingTypes))
for _, existingType := range existingTypes {
hasCategoryOfType[existingType] = true
}
if !hasCategoryOfType[model.SidebarCategoryFavorites] {
favoritesCategoryId := model.NewId()
// Create the SidebarChannels first since there's more opportunity for something to fail here
if err := s.migrateFavoritesToSidebarT(transaction, userId, teamId, favoritesCategoryId); err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to migrate favorites to sidebar")
}
if err := transaction.Insert(&model.SidebarCategory{
DisplayName: "Favorites", // This will be retranslated by the client into the user's locale
Id: favoritesCategoryId,
UserId: userId,
TeamId: teamId,
Sorting: model.SidebarCategorySortDefault,
SortOrder: model.DefaultSidebarSortOrderFavorites,
Type: model.SidebarCategoryFavorites,
}); err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert favorites category")
}
}
if !hasCategoryOfType[model.SidebarCategoryChannels] {
if err := transaction.Insert(&model.SidebarCategory{
DisplayName: "Channels", // This will be retranslateed by the client into the user's locale
Id: model.NewId(),
UserId: userId,
TeamId: teamId,
Sorting: model.SidebarCategorySortDefault,
SortOrder: model.DefaultSidebarSortOrderChannels,
Type: model.SidebarCategoryChannels,
}); err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert channels category")
}
}
if !hasCategoryOfType[model.SidebarCategoryDirectMessages] {
if err := transaction.Insert(&model.SidebarCategory{
DisplayName: "Direct Messages", // This will be retranslateed by the client into the user's locale
Id: model.NewId(),
UserId: userId,
TeamId: teamId,
Sorting: model.SidebarCategorySortRecent,
SortOrder: model.DefaultSidebarSortOrderDMs,
Type: model.SidebarCategoryDirectMessages,
}); err != nil {
return errors.Wrap(err, "createInitialSidebarCategoriesT: failed to insert direct messages category")
}
}
return nil
}
type userMembership struct {
UserId string
ChannelId string
CategoryId string
}
func (s SqlChannelStore) migrateMembershipToSidebar(transaction *gorp.Transaction, runningOrder *int64, sql string, args ...interface{}) ([]userMembership, error) {
var memberships []userMembership
if _, err := transaction.Select(&memberships, sql, args...); err != nil {
return nil, err
}
for _, favorite := range memberships {
sql, args, _ := s.getQueryBuilder().
Insert("SidebarChannels").
Columns("ChannelId", "UserId", "CategoryId", "SortOrder").
Values(favorite.ChannelId, favorite.UserId, favorite.CategoryId, *runningOrder).ToSql()
if _, err := transaction.Exec(sql, args...); err != nil && !IsUniqueConstraintError(err, []string{"UserId", "PRIMARY"}) {
return nil, err
}
*runningOrder = *runningOrder + model.MinimalSidebarSortDistance
}
if err := transaction.Commit(); err != nil {
return nil, err
}
return memberships, nil
}
func (s SqlChannelStore) migrateFavoritesToSidebarT(transaction *gorp.Transaction, userId, teamId, favoritesCategoryId string) error {
favoritesQuery, favoritesParams, _ := s.getQueryBuilder().
Select("Preferences.Name").
From("Preferences").
Join("Channels on Preferences.Name = Channels.Id").
Join("ChannelMembers on Preferences.Name = ChannelMembers.ChannelId and Preferences.UserId = ChannelMembers.UserId").
Where(sq.Eq{
"Preferences.UserId": userId,
"Preferences.Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL,
"Preferences.Value": "true",
}).
Where(sq.Or{
sq.Eq{"Channels.TeamId": teamId},
sq.Eq{"Channels.TeamId": ""},
}).
OrderBy(
"Channels.DisplayName",
"Channels.Name ASC",
).ToSql()
var favoriteChannelIds []string
if _, err := transaction.Select(&favoriteChannelIds, favoritesQuery, favoritesParams...); err != nil {
return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to get favorite channel IDs")
}
for i, channelId := range favoriteChannelIds {
if err := transaction.Insert(&model.SidebarChannel{
ChannelId: channelId,
CategoryId: favoritesCategoryId,
UserId: userId,
SortOrder: int64(i * model.MinimalSidebarSortDistance),
}); err != nil {
return errors.Wrap(err, "migrateFavoritesToSidebarT: unable to insert SidebarChannel")
}
}
return nil
}
// MigrateFavoritesToSidebarChannels populates the SidebarChannels table by analyzing existing user preferences for favorites
// **IMPORTANT** This function should only be called from the migration task and shouldn't be used by itself
func (s SqlChannelStore) MigrateFavoritesToSidebarChannels(lastUserId string, runningOrder int64) (map[string]interface{}, error) {
transaction, err := s.GetMaster().Begin()
if err != nil {
return nil, err
}
defer finalizeTransaction(transaction)
sb := s.
getQueryBuilder().
Select("Preferences.UserId", "Preferences.Name AS ChannelId", "SidebarCategories.Id AS CategoryId").
From("Preferences").
Where(sq.And{
sq.Eq{"Preferences.Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL},
sq.NotEq{"Preferences.Value": "false"},
sq.NotEq{"SidebarCategories.Id": nil},
sq.Gt{"Preferences.UserId": lastUserId},
}).
LeftJoin("Channels ON (Channels.Id=Preferences.Name)").
LeftJoin("SidebarCategories ON (SidebarCategories.UserId=Preferences.UserId AND SidebarCategories.Type='"+string(model.SidebarCategoryFavorites)+"' AND (SidebarCategories.TeamId=Channels.TeamId OR Channels.TeamId=''))").
OrderBy("Preferences.UserId", "Channels.Name DESC").
Limit(100)
sql, args, err := sb.ToSql()
if err != nil {
return nil, err
}
userFavorites, err := s.migrateMembershipToSidebar(transaction, &runningOrder, sql, args...)
if err != nil {
return nil, err
}
if len(userFavorites) == 0 {
return nil, nil
}
data := make(map[string]interface{})
data["UserId"] = userFavorites[len(userFavorites)-1].UserId
data["SortOrder"] = runningOrder
return data, nil
}
// MigratePublicChannels initializes the PublicChannels table with data created before this version
// of the Mattermost server kept it up-to-date.
func (s SqlChannelStore) MigratePublicChannels() error {
if _, err := s.GetMaster().Exec(`
INSERT INTO PublicChannels
(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose)
SELECT
c.Id, c.DeleteAt, c.TeamId, c.DisplayName, c.Name, c.Header, c.Purpose
FROM
Channels c
LEFT JOIN
PublicChannels pc ON (pc.Id = c.Id)
WHERE
c.Type = 'O'
AND pc.Id IS NULL
`); err != nil {
return err
}
return nil
}
func (s SqlChannelStore) upsertPublicChannelT(transaction *gorp.Transaction, channel *model.Channel) error {
publicChannel := &publicChannel{
Id: channel.Id,
DeleteAt: channel.DeleteAt,
TeamId: channel.TeamId,
DisplayName: channel.DisplayName,
Name: channel.Name,
Header: channel.Header,
Purpose: channel.Purpose,
}
if channel.Type != model.CHANNEL_OPEN {
if _, err := transaction.Delete(publicChannel); err != nil {
return errors.Wrap(err, "failed to delete public channel")
}
return nil
}
if s.DriverName() == model.DATABASE_DRIVER_MYSQL {
// Leverage native upsert for MySQL, since RowsAffected returns 0 if the row exists
// but no changes were made, breaking the update-then-insert paradigm below when
// the row already exists. (Postgres 9.4 doesn't support native upsert.)
if _, err := transaction.Exec(`
INSERT INTO
PublicChannels(Id, DeleteAt, TeamId, DisplayName, Name, Header, Purpose)
VALUES
(:Id, :DeleteAt, :TeamId, :DisplayName, :Name, :Header, :Purpose)
ON DUPLICATE KEY UPDATE
DeleteAt = :DeleteAt,
TeamId = :TeamId,
DisplayName = :DisplayName,
Name = :Name,
Header = :Header,
Purpose = :Purpose;
`, map[string]interface{}{
"Id": publicChannel.Id,
"DeleteAt": publicChannel.DeleteAt,
"TeamId": publicChannel.TeamId,
"DisplayName": publicChannel.DisplayName,
"Name": publicChannel.Name,
"Header": publicChannel.Header,
"Purpose": publicChannel.Purpose,
}); err != nil {
return errors.Wrap(err, "failed to insert public channel")
}
} else {
count, err := transaction.Update(publicChannel)
if err != nil {
return errors.Wrap(err, "failed to update public channel")
}
if count > 0 {
return nil
}
if err := transaction.Insert(publicChannel); err != nil {
return errors.Wrap(err, "failed to insert public channel")
}
}
return nil
}
// Save writes the (non-direct) channel channel to the database.
func (s SqlChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
if channel.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("Channel", "DeleteAt", channel.DeleteAt)
}
if channel.Type == model.CHANNEL_DIRECT {
return nil, store.NewErrInvalidInput("Channel", "Type", channel.Type)
}
var newChannel *model.Channel
transaction, err := s.GetMaster().Begin()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransaction(transaction)
newChannel, err = s.saveChannelT(transaction, channel, maxChannelsPerTeam)
if err != nil {
return newChannel, err
}
// Additionally propagate the write to the PublicChannels table.
if err = s.upsertPublicChannelT(transaction, newChannel); err != nil {
return nil, errors.Wrap(err, "upsert_public_channel")
}
if err = transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
// There are cases when in case of conflict, the original channel value is returned.
// So we return both and let the caller do the checks.
return newChannel, err
}
func (s SqlChannelStore) CreateDirectChannel(user *model.User, otherUser *model.User) (*model.Channel, error) {
channel := new(model.Channel)
channel.DisplayName = ""
channel.Name = model.GetDMNameFromIds(otherUser.Id, user.Id)
channel.Header = ""
channel.Type = model.CHANNEL_DIRECT
cm1 := &model.ChannelMember{
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
}
cm2 := &model.ChannelMember{
UserId: otherUser.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: otherUser.IsGuest(),
SchemeUser: !otherUser.IsGuest(),
}
return s.SaveDirectChannel(channel, cm1, cm2)
}
func (s SqlChannelStore) SaveDirectChannel(directchannel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) (*model.Channel, error) {
if directchannel.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("Channel", "DeleteAt", directchannel.DeleteAt)
}
if directchannel.Type != model.CHANNEL_DIRECT {
return nil, store.NewErrInvalidInput("Channel", "Type", directchannel.Type)
}
transaction, err := s.GetMaster().Begin()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransaction(transaction)
directchannel.TeamId = ""
newChannel, err := s.saveChannelT(transaction, directchannel, 0)
if err != nil {
return newChannel, err
}
// Members need new channel ID
member1.ChannelId = newChannel.Id
member2.ChannelId = newChannel.Id
if member1.UserId != member2.UserId {
_, err = s.saveMultipleMembersT(transaction, []*model.ChannelMember{member1, member2})
} else {
_, err = s.saveMemberT(transaction, member2)
}
if err != nil {
return nil, err
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return newChannel, nil
}
func (s SqlChannelStore) saveChannelT(transaction *gorp.Transaction, channel *model.Channel, maxChannelsPerTeam int64) (*model.Channel, error) {
if len(channel.Id) > 0 {
return nil, store.NewErrInvalidInput("Channel", "Id", channel.Id)
}
channel.PreSave()
if err := channel.IsValid(); err != nil { // TODO: this needs to return plain error in v6.
return nil, err // we just pass through the error as-is for now.
}
if channel.Type != model.CHANNEL_DIRECT && channel.Type != model.CHANNEL_GROUP && maxChannelsPerTeam >= 0 {
if count, err := transaction.SelectInt("SELECT COUNT(0) FROM Channels WHERE TeamId = :TeamId AND DeleteAt = 0 AND (Type = 'O' OR Type = 'P')", map[string]interface{}{"TeamId": channel.TeamId}); err != nil {
return nil, errors.Wrapf(err, "save_channel_count: teamId=%s", channel.TeamId)
} else if count >= maxChannelsPerTeam {
return nil, store.NewErrLimitExceeded("channels_per_team", int(count), "teamId="+channel.TeamId)
}
}
if err := transaction.Insert(channel); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "channels_name_teamid_key"}) {
dupChannel := model.Channel{}
s.GetMaster().SelectOne(&dupChannel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name = :Name", map[string]interface{}{"TeamId": channel.TeamId, "Name": channel.Name})
return &dupChannel, store.NewErrConflict("Channel", err, "id="+channel.Id)
}
return nil, errors.Wrapf(err, "save_channel: id=%s", channel.Id)
}
return channel, nil
}
// Update writes the updated channel to the database.
func (s SqlChannelStore) Update(channel *model.Channel) (*model.Channel, error) {
transaction, err := s.GetMaster().Begin()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransaction(transaction)
updatedChannel, appErr := s.updateChannelT(transaction, channel)
if appErr != nil {
return nil, appErr
}
// Additionally propagate the write to the PublicChannels table.
if err := s.upsertPublicChannelT(transaction, updatedChannel); err != nil {
return nil, errors.Wrap(err, "upsertPublicChannelT: failed to upsert channel")
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return updatedChannel, nil
}
func (s SqlChannelStore) updateChannelT(transaction *gorp.Transaction, channel *model.Channel) (*model.Channel, error) {
channel.PreUpdate()
if channel.DeleteAt != 0 {
return nil, store.NewErrInvalidInput("Channel", "DeleteAt", channel.DeleteAt)
}
if err := channel.IsValid(); err != nil {
return nil, err
}
count, err := transaction.Update(channel)
if err != nil {
if IsUniqueConstraintError(err, []string{"Name", "channels_name_teamid_key"}) {
dupChannel := model.Channel{}
s.GetReplica().SelectOne(&dupChannel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name= :Name AND DeleteAt > 0", map[string]interface{}{"TeamId": channel.TeamId, "Name": channel.Name})
if dupChannel.DeleteAt > 0 {
return nil, store.NewErrInvalidInput("Channel", "Id", channel.Id)
}
return nil, store.NewErrInvalidInput("Channel", "Id", channel.Id)
}
return nil, errors.Wrapf(err, "failed to update channel with id=%s", channel.Id)
}
if count > 1 {
return nil, fmt.Errorf("the expected number of channels to be updated is <=1 but was %d", count)
}
return channel, nil
}
func (s SqlChannelStore) GetChannelUnread(channelId, userId string) (*model.ChannelUnread, *model.AppError) {
var unreadChannel model.ChannelUnread
err := s.GetReplica().SelectOne(&unreadChannel,
`SELECT
Channels.TeamId TeamId, Channels.Id ChannelId, (Channels.TotalMsgCount - ChannelMembers.MsgCount) MsgCount, ChannelMembers.MentionCount MentionCount, ChannelMembers.NotifyProps NotifyProps
FROM
Channels, ChannelMembers
WHERE
Id = ChannelId
AND Id = :ChannelId
AND UserId = :UserId
AND DeleteAt = 0`,
map[string]interface{}{"ChannelId": channelId, "UserId": userId})
if err != nil {
if err == sql.ErrNoRows {
return nil, model.NewAppError("SqlChannelStore.GetChannelUnread", "store.sql_channel.get_unread.app_error", nil, "channelId="+channelId+" "+err.Error(), http.StatusNotFound)
}
return nil, model.NewAppError("SqlChannelStore.GetChannelUnread", "store.sql_channel.get_unread.app_error", nil, "channelId="+channelId+" "+err.Error(), http.StatusInternalServerError)
}
return &unreadChannel, nil
}
func (s SqlChannelStore) InvalidateChannel(id string) {
}
func (s SqlChannelStore) InvalidateChannelByName(teamId, name string) {
channelByNameCache.Remove(teamId + name)
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("Channel by Name - Remove by TeamId and Name")
}
}
func (s SqlChannelStore) Get(id string, allowFromCache bool) (*model.Channel, error) {
return s.get(id, false, allowFromCache)
}
func (s SqlChannelStore) GetPinnedPosts(channelId string) (*model.PostList, *model.AppError) {
pl := model.NewPostList()
var posts []*model.Post
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 IsPinned = true AND ChannelId = :ChannelId AND DeleteAt = 0 ORDER BY CreateAt ASC", map[string]interface{}{"ChannelId": channelId}); err != nil {
return nil, model.NewAppError("SqlPostStore.GetPinnedPosts", "store.sql_channel.pinned_posts.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, post := range posts {
pl.AddPost(post)
pl.AddOrder(post.Id)
}
return pl, nil
}
func (s SqlChannelStore) GetFromMaster(id string) (*model.Channel, error) {
return s.get(id, true, false)
}
func (s SqlChannelStore) get(id string, master bool, allowFromCache bool) (*model.Channel, error) {
var db *gorp.DbMap
if master {
db = s.GetMaster()
} else {
db = s.GetReplica()
}
obj, err := db.Get(model.Channel{}, id)
if err != nil {
return nil, errors.Wrapf(err, "failed to find channel with id = %s", id)
}
if obj == nil {
return nil, store.NewErrNotFound("Channel", id)
}
ch := obj.(*model.Channel)
return ch, nil
}
// Delete records the given deleted timestamp to the channel in question.
func (s SqlChannelStore) Delete(channelId string, time int64) error {
return s.SetDeleteAt(channelId, time, time)
}
// Restore reverts a previous deleted timestamp from the channel in question.
func (s SqlChannelStore) Restore(channelId string, time int64) error {
return s.SetDeleteAt(channelId, 0, time)
}
// SetDeleteAt records the given deleted and updated timestamp to the channel in question.
func (s SqlChannelStore) SetDeleteAt(channelId string, deleteAt, updateAt int64) error {
defer s.InvalidateChannel(channelId)
transaction, err := s.GetMaster().Begin()
if err != nil {
return errors.Wrap(err, "SetDeleteAt: begin_transaction")
}
defer finalizeTransaction(transaction)
err = s.setDeleteAtT(transaction, channelId, deleteAt, updateAt)
if err != nil {
return errors.Wrap(err, "setDeleteAtT")
}
// Additionally propagate the write to the PublicChannels table.
if _, err := transaction.Exec(`
UPDATE
PublicChannels
SET
DeleteAt = :DeleteAt
WHERE
Id = :ChannelId
`, map[string]interface{}{
"DeleteAt": deleteAt,
"ChannelId": channelId,
}); err != nil {
return errors.Wrapf(err, "failed to delete public channels with id=%s", channelId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrapf(err, "SetDeleteAt: commit_transaction")
}
return nil
}
func (s SqlChannelStore) setDeleteAtT(transaction *gorp.Transaction, channelId string, deleteAt, updateAt int64) error {
_, err := transaction.Exec("Update Channels SET DeleteAt = :DeleteAt, UpdateAt = :UpdateAt WHERE Id = :ChannelId", map[string]interface{}{"DeleteAt": deleteAt, "UpdateAt": updateAt, "ChannelId": channelId})
if err != nil {
return errors.Wrapf(err, "failed to delete channel with id=%s", channelId)
}
return nil
}
// PermanentDeleteByTeam removes all channels for the given team from the database.
func (s SqlChannelStore) PermanentDeleteByTeam(teamId string) error {
transaction, err := s.GetMaster().Begin()
if err != nil {
return errors.Wrap(err, "PermanentDeleteByTeam: begin_transaction")
}
defer finalizeTransaction(transaction)
if err := s.permanentDeleteByTeamtT(transaction, teamId); err != nil {
return errors.Wrap(err, "permanentDeleteByTeamtT")
}
// Additionally propagate the deletions to the PublicChannels table.
if _, err := transaction.Exec(`
DELETE FROM
PublicChannels
WHERE
TeamId = :TeamId
`, map[string]interface{}{
"TeamId": teamId,
}); err != nil {
return errors.Wrapf(err, "failed to delete public channels by team with teamId=%s", teamId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "PermanentDeleteByTeam: commit_transaction")
}
return nil
}
func (s SqlChannelStore) permanentDeleteByTeamtT(transaction *gorp.Transaction, teamId string) error {
if _, err := transaction.Exec("DELETE FROM Channels WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}); err != nil {
return errors.Wrapf(err, "failed to delete channel by team with teamId=%s", teamId)
}
return nil
}
// PermanentDelete removes the given channel from the database.
func (s SqlChannelStore) PermanentDelete(channelId string) error {
transaction, err := s.GetMaster().Begin()
if err != nil {
return errors.Wrap(err, "PermanentDelete: begin_transaction")
}
defer finalizeTransaction(transaction)
if err := s.permanentDeleteT(transaction, channelId); err != nil {
return errors.Wrap(err, "permanentDeleteT")
}
// Additionally propagate the deletion to the PublicChannels table.
if _, err := transaction.Exec(`
DELETE FROM
PublicChannels
WHERE
Id = :ChannelId
`, map[string]interface{}{
"ChannelId": channelId,
}); err != nil {
return errors.Wrapf(err, "failed to delete public channels with id=%s", channelId)
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "PermanentDelete: commit_transaction")
}
return nil
}
func (s SqlChannelStore) permanentDeleteT(transaction *gorp.Transaction, channelId string) error {
if _, err := transaction.Exec("DELETE FROM Channels WHERE Id = :ChannelId", map[string]interface{}{"ChannelId": channelId}); err != nil {
return errors.Wrapf(err, "failed to delete channel with id=%s", channelId)
}
return nil
}
func (s SqlChannelStore) PermanentDeleteMembersByChannel(channelId string) *model.AppError {
_, err := s.GetMaster().Exec("DELETE FROM ChannelMembers WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channelId})
if err != nil {
return model.NewAppError("SqlChannelStore.RemoveAllMembersByChannel", "store.sql_channel.remove_member.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) GetChannels(teamId string, userId string, includeDeleted bool, lastDeleteAt int) (*model.ChannelList, error) {
query := s.getQueryBuilder().
Select("Channels.*").
From("Channels, ChannelMembers").
Where(
sq.And{
sq.Expr("Id = ChannelId"),
sq.Eq{"UserId": userId},
sq.Or{
sq.Eq{"TeamId": teamId},
sq.Eq{"TeamId": ""},
},
},
).
OrderBy("DisplayName")
if includeDeleted {
if lastDeleteAt != 0 {
// We filter by non-archived, and archived >= a timestamp.
query = query.Where(sq.Or{
sq.Eq{"DeleteAt": 0},
sq.GtOrEq{"DeleteAt": lastDeleteAt},
})
}
// If lastDeleteAt is not set, we include everything. That means no filter is needed.
} else {
// Don't include archived channels.
query = query.Where(sq.Eq{"DeleteAt": 0})
}
channels := &model.ChannelList{}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrapf(err, "getchannels_tosql")
}
_, err = s.GetReplica().Select(channels, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channels with TeamId=%s and UserId=%s", teamId, userId)
}
if len(*channels) == 0 {
return nil, store.NewErrNotFound("Channel", "userId="+userId)
}
return channels, nil
}
func (s SqlChannelStore) GetAllChannels(offset, limit int, opts store.ChannelSearchOpts) (*model.ChannelListWithTeamData, error) {
query := s.getAllChannelsQuery(opts, false)
query = query.OrderBy("c.DisplayName, Teams.DisplayName").Limit(uint64(limit)).Offset(uint64(offset))
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to create query")
}
data := &model.ChannelListWithTeamData{}
_, err = s.GetReplica().Select(data, queryString, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to get all channels")
}
return data, nil
}
func (s SqlChannelStore) GetAllChannelsCount(opts store.ChannelSearchOpts) (int64, error) {
query := s.getAllChannelsQuery(opts, true)
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "failed to create query")
}
count, err := s.GetReplica().SelectInt(queryString, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count all channels")
}
return count, nil
}
func (s SqlChannelStore) getAllChannelsQuery(opts store.ChannelSearchOpts, forCount bool) sq.SelectBuilder {
var selectStr string
if forCount {
selectStr = "count(c.Id)"
} else {
selectStr = "c.*, Teams.DisplayName AS TeamDisplayName, Teams.Name AS TeamName, Teams.UpdateAt AS TeamUpdateAt"
}
query := s.getQueryBuilder().
Select(selectStr).
From("Channels AS c").
Where(sq.Eq{"c.Type": []string{model.CHANNEL_PRIVATE, model.CHANNEL_OPEN}})
if !forCount {
query = query.Join("Teams ON Teams.Id = c.TeamId")
}
if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": int(0)})
}
if len(opts.NotAssociatedToGroup) > 0 {
query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup)
}
if len(opts.ExcludeChannelNames) > 0 {
query = query.Where(sq.NotEq{"c.Name": opts.ExcludeChannelNames})
}
return query
}
func (s SqlChannelStore) GetMoreChannels(teamId string, userId string, offset int, limit int) (*model.ChannelList, error) {
channels := &model.ChannelList{}
_, err := s.GetReplica().Select(channels, `
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels c ON (c.Id = Channels.Id)
WHERE
c.TeamId = :TeamId
AND c.DeleteAt = 0
AND c.Id NOT IN (
SELECT
c.Id
FROM
PublicChannels c
JOIN
ChannelMembers cm ON (cm.ChannelId = c.Id)
WHERE
c.TeamId = :TeamId
AND cm.UserId = :UserId
AND c.DeleteAt = 0
)
ORDER BY
c.DisplayName
LIMIT :Limit
OFFSET :Offset
`, map[string]interface{}{
"TeamId": teamId,
"UserId": userId,
"Limit": limit,
"Offset": offset,
})
if err != nil {
return nil, errors.Wrapf(err, "failed getting channels with teamId=%s and userId=%s", teamId, userId)
}
return channels, nil
}
func (s SqlChannelStore) GetPrivateChannelsForTeam(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError) {
channels := &model.ChannelList{}
query := s.getQueryBuilder().
Select("*").
From("Channels").
Where(sq.Eq{"Type": model.CHANNEL_PRIVATE, "TeamId": teamId, "DeleteAt": 0}).
OrderBy("DisplayName").
Limit(uint64(limit)).
Offset(uint64(offset))
sql, args, err := query.ToSql()
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetPrivateChannelsForTeam", "store.sql_channel.get_private_channels.get.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError)
}
_, err = s.GetReplica().Select(channels, sql, args...)
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetPrivateChannelsForTeam", "store.sql_channel.get_private_channels.get.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError)
}
return channels, nil
}
func (s SqlChannelStore) GetPublicChannelsForTeam(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError) {
channels := &model.ChannelList{}
_, err := s.GetReplica().Select(channels, `
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels pc ON (pc.Id = Channels.Id)
WHERE
pc.TeamId = :TeamId
AND pc.DeleteAt = 0
ORDER BY pc.DisplayName
LIMIT :Limit
OFFSET :Offset
`, map[string]interface{}{
"TeamId": teamId,
"Limit": limit,
"Offset": offset,
})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetPublicChannelsForTeam", "store.sql_channel.get_public_channels.get.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError)
}
return channels, nil
}
func (s SqlChannelStore) GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) (*model.ChannelList, *model.AppError) {
props := make(map[string]interface{})
props["teamId"] = teamId
idQuery := ""
for index, channelId := range channelIds {
if len(idQuery) > 0 {
idQuery += ", "
}
props["channelId"+strconv.Itoa(index)] = channelId
idQuery += ":channelId" + strconv.Itoa(index)
}
data := &model.ChannelList{}
_, err := s.GetReplica().Select(data, `
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels pc ON (pc.Id = Channels.Id)
WHERE
pc.TeamId = :teamId
AND pc.DeleteAt = 0
AND pc.Id IN (`+idQuery+`)
ORDER BY pc.DisplayName
`, props)
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetPublicChannelsByIdsForTeam", "store.sql_channel.get_channels_by_ids.get.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(*data) == 0 {
return nil, model.NewAppError("SqlChannelStore.GetPublicChannelsByIdsForTeam", "store.sql_channel.get_channels_by_ids.not_found.app_error", nil, "", http.StatusNotFound)
}
return data, nil
}
type channelIdWithCountAndUpdateAt struct {
Id string
TotalMsgCount int64
UpdateAt int64
}
func (s SqlChannelStore) GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, *model.AppError) {
var data []channelIdWithCountAndUpdateAt
_, err := s.GetReplica().Select(&data, "SELECT Id, TotalMsgCount, UpdateAt FROM Channels WHERE Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId) AND (TeamId = :TeamId OR TeamId = '') AND DeleteAt = 0 ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId, "UserId": userId})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetChannelCounts", "store.sql_channel.get_channel_counts.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error(), http.StatusInternalServerError)
}
counts := &model.ChannelCounts{Counts: make(map[string]int64), UpdateTimes: make(map[string]int64)}
for i := range data {
v := data[i]
counts.Counts[v.Id] = v.TotalMsgCount
counts.UpdateTimes[v.Id] = v.UpdateAt
}
return counts, nil
}
func (s SqlChannelStore) GetTeamChannels(teamId string) (*model.ChannelList, *model.AppError) {
data := &model.ChannelList{}
_, err := s.GetReplica().Select(data, "SELECT * FROM Channels WHERE TeamId = :TeamId And Type != 'D' ORDER BY DisplayName", map[string]interface{}{"TeamId": teamId})
if err != nil {
// TODO: This error key would go away once this store method is migrated to return plain errors
return nil, model.NewAppError("SqlChannelStore.GetTeamChannels", "app.channel.get_channels.get.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError)
}
if len(*data) == 0 {
// TODO: This error key would go away once this store method is migrated to return plain errors
return nil, model.NewAppError("SqlChannelStore.GetTeamChannels", "app.channel.get_channels.not_found.app_error", nil, "teamId="+teamId, http.StatusNotFound)
}
return data, nil
}
func (s SqlChannelStore) GetByName(teamId string, name string, allowFromCache bool) (*model.Channel, error) {
return s.getByName(teamId, name, false, allowFromCache)
}
func (s SqlChannelStore) GetByNames(teamId string, names []string, allowFromCache bool) ([]*model.Channel, error) {
var channels []*model.Channel
if allowFromCache {
var misses []string
visited := make(map[string]struct{})
for _, name := range names {
if _, ok := visited[name]; ok {
continue
}
visited[name] = struct{}{}
var cacheItem *model.Channel
if err := channelByNameCache.Get(teamId+name, &cacheItem); err == nil {
channels = append(channels, cacheItem)
} else {
misses = append(misses, name)
}
}
names = misses
}
if len(names) > 0 {
props := map[string]interface{}{}
var namePlaceholders []string
for _, name := range names {
key := fmt.Sprintf("Name%v", len(namePlaceholders))
props[key] = name
namePlaceholders = append(namePlaceholders, ":"+key)
}
var query string
if teamId == "" {
query = `SELECT * FROM Channels WHERE Name IN (` + strings.Join(namePlaceholders, ", ") + `) AND DeleteAt = 0`
} else {
props["TeamId"] = teamId
query = `SELECT * FROM Channels WHERE Name IN (` + strings.Join(namePlaceholders, ", ") + `) AND TeamId = :TeamId AND DeleteAt = 0`
}
var dbChannels []*model.Channel
if _, err := s.GetReplica().Select(&dbChannels, query, props); err != nil && err != sql.ErrNoRows {
msg := fmt.Sprintf("failed to get channels with names=%v", names)
if teamId != "" {
msg += fmt.Sprintf("teamId=%s", teamId)
}
return nil, errors.Wrap(err, msg)
}
for _, channel := range dbChannels {
channelByNameCache.SetWithExpiry(teamId+channel.Name, channel, CHANNEL_CACHE_DURATION)
channels = append(channels, channel)
}
// Not all channels are in cache. Increment aggregate miss counter.
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("Channel By Name - Aggregate")
}
} else {
// All of the channel names are in cache. Increment aggregate hit counter.
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("Channel By Name - Aggregate")
}
}
return channels, nil
}
func (s SqlChannelStore) GetByNameIncludeDeleted(teamId string, name string, allowFromCache bool) (*model.Channel, error) {
return s.getByName(teamId, name, true, allowFromCache)
}
func (s SqlChannelStore) getByName(teamId string, name string, includeDeleted bool, allowFromCache bool) (*model.Channel, error) {
var query string
if includeDeleted {
query = "SELECT * FROM Channels WHERE (TeamId = :TeamId OR TeamId = '') AND Name = :Name"
} else {
query = "SELECT * FROM Channels WHERE (TeamId = :TeamId OR TeamId = '') AND Name = :Name AND DeleteAt = 0"
}
channel := model.Channel{}
if allowFromCache {
var cacheItem *model.Channel
if err := channelByNameCache.Get(teamId+name, &cacheItem); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("Channel By Name")
}
return cacheItem, nil
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("Channel By Name")
}
}
if err := s.GetReplica().SelectOne(&channel, query, map[string]interface{}{"TeamId": teamId, "Name": name}); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("TeamId=%s&Name=%s", teamId, name))
}
return nil, errors.Wrapf(err, "failed to find channel with TeamId=%s and Name=%s", teamId, name)
}
channelByNameCache.SetWithExpiry(teamId+name, &channel, CHANNEL_CACHE_DURATION)
return &channel, nil
}
func (s SqlChannelStore) GetDeletedByName(teamId string, name string) (*model.Channel, error) {
channel := model.Channel{}
if err := s.GetReplica().SelectOne(&channel, "SELECT * FROM Channels WHERE (TeamId = :TeamId OR TeamId = '') AND Name = :Name AND DeleteAt != 0", map[string]interface{}{"TeamId": teamId, "Name": name}); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("name=%s", name))
}
return nil, errors.Wrapf(err, "failed to get channel by teamId=%s and name=%s", teamId, name)
}
return &channel, nil
}
func (s SqlChannelStore) GetDeleted(teamId string, offset int, limit int, userId string) (*model.ChannelList, error) {
channels := &model.ChannelList{}
query := `
SELECT * FROM Channels
WHERE (TeamId = :TeamId OR TeamId = '')
AND DeleteAt != 0
AND Type != 'P'
UNION
SELECT * FROM Channels
WHERE (TeamId = :TeamId OR TeamId = '')
AND DeleteAt != 0
AND Type = 'P'
AND Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId)
ORDER BY DisplayName LIMIT :Limit OFFSET :Offset
`
if _, err := s.GetReplica().Select(channels, query, map[string]interface{}{"TeamId": teamId, "Limit": limit, "Offset": offset, "UserId": userId}); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("TeamId=%s,UserId=%s", teamId, userId))
}
return nil, errors.Wrapf(err, "failed to get deleted channels with TeamId=%s and UserId=%s", teamId, userId)
}
return channels, nil
}
var CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY = `
SELECT
ChannelMembers.*,
TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole,
TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole,
ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
FROM
ChannelMembers
INNER JOIN
Channels ON ChannelMembers.ChannelId = Channels.Id
LEFT JOIN
Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id
LEFT JOIN
Teams ON Channels.TeamId = Teams.Id
LEFT JOIN
Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
`
func (s SqlChannelStore) SaveMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, *model.AppError) {
for _, member := range members {
defer s.InvalidateAllChannelMembersForUser(member.UserId)
}
transaction, err := s.GetMaster().Begin()
if err != nil {
return nil, model.NewAppError("SqlChannelStore.SaveMember", "store.sql_channel.save_member.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
newMembers, err := s.saveMultipleMembersT(transaction, members)
if err != nil { // TODO: this will go away once SaveMultipleMembers is migrated too.
var cErr *store.ErrConflict
var appErr *model.AppError
switch {
case errors.As(err, &cErr):
switch cErr.Resource {
case "ChannelMembers":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_member.exists.app_error", nil, cErr.Error(), http.StatusBadRequest)
}
case errors.As(err, &appErr): // in case we haven't converted to plain error.
return nil, appErr
default: // last fallback in case it doesn't map to an existing app error.
// TODO: This error key would go away once this store method is migrated to return plain errors
return nil, model.NewAppError("CreateDirectChannel", "app.channel.create_direct_channel.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
}
if err := transaction.Commit(); err != nil {
return nil, model.NewAppError("SqlChannelStore.SaveMember", "store.sql_channel.save_member.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return newMembers, nil
}
func (s SqlChannelStore) SaveMember(member *model.ChannelMember) (*model.ChannelMember, *model.AppError) {
newMembers, appErr := s.SaveMultipleMembers([]*model.ChannelMember{member})
if appErr != nil {
return nil, appErr
}
return newMembers[0], nil
}
func (s SqlChannelStore) saveMultipleMembersT(transaction *gorp.Transaction, members []*model.ChannelMember) ([]*model.ChannelMember, error) {
newChannelMembers := map[string]int{}
users := map[string]bool{}
for _, member := range members {
if val, ok := newChannelMembers[member.ChannelId]; val < 1 || !ok {
newChannelMembers[member.ChannelId] = 1
} else {
newChannelMembers[member.ChannelId]++
}
users[member.UserId] = true
member.PreSave()
if err := member.IsValid(); err != nil { // TODO: this needs to return plain error in v6.
return nil, err
}
}
channels := []string{}
for channel := range newChannelMembers {
channels = append(channels, channel)
}
defaultChannelRolesByChannel := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
channelRolesQuery := s.getQueryBuilder().
Select(
"Channels.Id as Id",
"ChannelScheme.DefaultChannelGuestRole as Guest",
"ChannelScheme.DefaultChannelUserRole as User",
"ChannelScheme.DefaultChannelAdminRole as Admin",
).
From("Channels").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
Where(sq.Eq{"Channels.Id": channels})
channelRolesSql, channelRolesArgs, err := channelRolesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_roles_tosql")
}
var defaultChannelsRoles []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}
_, err = s.GetMaster().Select(&defaultChannelsRoles, channelRolesSql, channelRolesArgs...)
if err != nil {
return nil, errors.Wrap(err, "default_channel_roles_select")
}
for _, defaultRoles := range defaultChannelsRoles {
defaultChannelRolesByChannel[defaultRoles.Id] = defaultRoles
}
defaultTeamRolesByChannel := map[string]struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}{}
teamRolesQuery := s.getQueryBuilder().
Select(
"Channels.Id as Id",
"TeamScheme.DefaultChannelGuestRole as Guest",
"TeamScheme.DefaultChannelUserRole as User",
"TeamScheme.DefaultChannelAdminRole as Admin",
).
From("Channels").
LeftJoin("Teams ON Teams.Id = Channels.TeamId").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"Channels.Id": channels})
teamRolesSql, teamRolesArgs, err := teamRolesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "team_roles_tosql")
}
var defaultTeamsRoles []struct {
Id string
Guest sql.NullString
User sql.NullString
Admin sql.NullString
}
_, err = s.GetMaster().Select(&defaultTeamsRoles, teamRolesSql, teamRolesArgs...)
if err != nil {
return nil, errors.Wrap(err, "default_team_roles_select")
}
for _, defaultRoles := range defaultTeamsRoles {
defaultTeamRolesByChannel[defaultRoles.Id] = defaultRoles
}
query := s.getQueryBuilder().Insert("ChannelMembers").Columns(channelMemberSliceColumns()...)
for _, member := range members {
query = query.Values(channelMemberToSlice(member)...)
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "channel_members_tosql")
}
if _, err := s.GetMaster().Exec(sql, args...); err != nil {
if IsUniqueConstraintError(err, []string{"ChannelId", "channelmembers_pkey", "PRIMARY"}) {
return nil, store.NewErrConflict("ChannelMembers", err, "")
}
return nil, errors.Wrap(err, "channel_members_save")
}
newMembers := []*model.ChannelMember{}
for _, member := range members {
defaultTeamGuestRole := defaultTeamRolesByChannel[member.ChannelId].Guest.String
defaultTeamUserRole := defaultTeamRolesByChannel[member.ChannelId].User.String
defaultTeamAdminRole := defaultTeamRolesByChannel[member.ChannelId].Admin.String
defaultChannelGuestRole := defaultChannelRolesByChannel[member.ChannelId].Guest.String
defaultChannelUserRole := defaultChannelRolesByChannel[member.ChannelId].User.String
defaultChannelAdminRole := defaultChannelRolesByChannel[member.ChannelId].Admin.String
rolesResult := getChannelRoles(
member.SchemeGuest, member.SchemeUser, member.SchemeAdmin,
defaultTeamGuestRole, defaultTeamUserRole, defaultTeamAdminRole,
defaultChannelGuestRole, defaultChannelUserRole, defaultChannelAdminRole,
strings.Fields(member.ExplicitRoles),
)
newMember := *member
newMember.SchemeGuest = rolesResult.schemeGuest
newMember.SchemeUser = rolesResult.schemeUser
newMember.SchemeAdmin = rolesResult.schemeAdmin
newMember.Roles = strings.Join(rolesResult.roles, " ")
newMember.ExplicitRoles = strings.Join(rolesResult.explicitRoles, " ")
newMembers = append(newMembers, &newMember)
}
return newMembers, nil
}
func (s SqlChannelStore) saveMemberT(transaction *gorp.Transaction, member *model.ChannelMember) (*model.ChannelMember, error) {
members, err := s.saveMultipleMembersT(transaction, []*model.ChannelMember{member})
if err != nil {
return nil, err
}
return members[0], nil
}
func (s SqlChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([]*model.ChannelMember, *model.AppError) {
for _, member := range members {
member.PreUpdate()
if err := member.IsValid(); err != nil {
return nil, err
}
}
var transaction *gorp.Transaction
var err error
if transaction, err = s.GetMaster().Begin(); err != nil {
return nil, model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
updatedMembers := []*model.ChannelMember{}
for _, member := range members {
if _, err := transaction.Update(NewChannelMemberFromModel(member)); err != nil {
return nil, model.NewAppError("SqlChannelStore.UpdateMember", "store.sql_channel.update_member.app_error", nil, "channel_id="+member.ChannelId+", "+"user_id="+member.UserId+", "+err.Error(), http.StatusInternalServerError)
}
// TODO: Get this out of the transaction when is possible
var dbMember channelMemberWithSchemeRoles
if err := transaction.SelectOne(&dbMember, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId = :UserId", map[string]interface{}{"ChannelId": member.ChannelId, "UserId": member.UserId}); err != nil {
if err == sql.ErrNoRows {
return nil, model.NewAppError("SqlChannelStore.GetMember", store.MISSING_CHANNEL_MEMBER_ERROR, nil, "channel_id="+member.ChannelId+"user_id="+member.UserId+","+err.Error(), http.StatusNotFound)
}
return nil, model.NewAppError("SqlChannelStore.GetMember", "store.sql_channel.get_member.app_error", nil, "channel_id="+member.ChannelId+"user_id="+member.UserId+","+err.Error(), http.StatusInternalServerError)
}
updatedMembers = append(updatedMembers, dbMember.ToModel())
}
if err := transaction.Commit(); err != nil {
return nil, model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return updatedMembers, nil
}
func (s SqlChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, *model.AppError) {
updatedMembers, err := s.UpdateMultipleMembers([]*model.ChannelMember{member})
if err != nil {
return nil, err
}
return updatedMembers[0], nil
}
func (s SqlChannelStore) GetMembers(channelId string, offset, limit int) (*model.ChannelMembers, *model.AppError) {
var dbMembers channelMemberWithSchemeRolesList
_, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelId = :ChannelId LIMIT :Limit OFFSET :Offset", map[string]interface{}{"ChannelId": channelId, "Limit": limit, "Offset": offset})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetMembers", "store.sql_channel.get_members.app_error", nil, "channel_id="+channelId+","+err.Error(), http.StatusInternalServerError)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetChannelMembersTimezones(channelId string) ([]model.StringMap, *model.AppError) {
var dbMembersTimezone []model.StringMap
_, err := s.GetReplica().Select(&dbMembersTimezone, `
SELECT
Users.Timezone
FROM
ChannelMembers
LEFT JOIN
Users ON ChannelMembers.UserId = Id
WHERE ChannelId = :ChannelId
`, map[string]interface{}{"ChannelId": channelId})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetChannelMembersTimezones", "store.sql_channel.get_members.app_error", nil, "channel_id="+channelId+","+err.Error(), http.StatusInternalServerError)
}
return dbMembersTimezone, nil
}
func (s SqlChannelStore) GetMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
var dbMember channelMemberWithSchemeRoles
if err := s.GetReplica().SelectOne(&dbMember, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId = :UserId", map[string]interface{}{"ChannelId": channelId, "UserId": userId}); err != nil {
if err == sql.ErrNoRows {
return nil, model.NewAppError("SqlChannelStore.GetMember", store.MISSING_CHANNEL_MEMBER_ERROR, nil, "channel_id="+channelId+"user_id="+userId+","+err.Error(), http.StatusNotFound)
}
return nil, model.NewAppError("SqlChannelStore.GetMember", "store.sql_channel.get_member.app_error", nil, "channel_id="+channelId+"user_id="+userId+","+err.Error(), http.StatusInternalServerError)
}
return dbMember.ToModel(), nil
}
func (s SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) {
allChannelMembersForUserCache.Remove(userId)
allChannelMembersForUserCache.Remove(userId + "_deleted")
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Remove by UserId")
}
}
func (s SqlChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool {
var ids map[string]string
if err := allChannelMembersForUserCache.Get(userId, &ids); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("All Channel Members for User")
}
if _, ok := ids[channelId]; ok {
return true
}
return false
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("All Channel Members for User")
}
ids, err := s.GetAllChannelMembersForUser(userId, true, false)
if err != nil {
mlog.Error("Error getting all channel members for user", mlog.Err(err))
return false
}
if _, ok := ids[channelId]; ok {
return true
}
return false
}
func (s SqlChannelStore) GetMemberForPost(postId string, userId string) (*model.ChannelMember, *model.AppError) {
var dbMember channelMemberWithSchemeRoles
query := `
SELECT
ChannelMembers.*,
TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole,
TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole,
ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
FROM
ChannelMembers
INNER JOIN
Posts ON ChannelMembers.ChannelId = Posts.ChannelId
INNER JOIN
Channels ON ChannelMembers.ChannelId = Channels.Id
LEFT JOIN
Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id
LEFT JOIN
Teams ON Channels.TeamId = Teams.Id
LEFT JOIN
Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id
WHERE
ChannelMembers.UserId = :UserId
AND
Posts.Id = :PostId`
if err := s.GetReplica().SelectOne(&dbMember, query, map[string]interface{}{"UserId": userId, "PostId": postId}); err != nil {
return nil, model.NewAppError("SqlChannelStore.GetMemberForPost", "store.sql_channel.get_member_for_post.app_error", nil, "postId="+postId+", err="+err.Error(), http.StatusInternalServerError)
}
return dbMember.ToModel(), nil
}
func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCache bool, includeDeleted bool) (map[string]string, *model.AppError) {
cache_key := userId
if includeDeleted {
cache_key += "_deleted"
}
if allowFromCache {
var ids map[string]string
if err := allChannelMembersForUserCache.Get(cache_key, &ids); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("All Channel Members for User")
}
return ids, nil
}
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("All Channel Members for User")
}
failure := func(err error) *model.AppError {
// TODO: This error key would go away once this store method is migrated to return plain errors
return model.NewAppError(
"SqlChannelStore.GetAllChannelMembersForUser",
"app.channel.get_channels.get.app_error",
nil,
"userId="+userId+", err="+err.Error(),
http.StatusInternalServerError,
)
}
query := s.getQueryBuilder().
Select(`
ChannelMembers.ChannelId, ChannelMembers.Roles, ChannelMembers.SchemeGuest,
ChannelMembers.SchemeUser, ChannelMembers.SchemeAdmin,
TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole,
TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole,
TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole,
ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole,
ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole,
ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole
`).
From("ChannelMembers").
Join("Channels ON ChannelMembers.ChannelId = Channels.Id").
LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id").
LeftJoin("Teams ON Channels.TeamId = Teams.Id").
LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id").
Where(sq.Eq{"ChannelMembers.UserId": userId})
if !includeDeleted {
query = query.Where(sq.Eq{"Channels.DeleteAt": 0})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, failure(err)
}
rows, err := s.GetReplica().Db.Query(queryString, args...)
if err != nil {
return nil, failure(err)
}
var data allChannelMembers
defer rows.Close()
for rows.Next() {
var cm allChannelMember
err = rows.Scan(
&cm.ChannelId, &cm.Roles, &cm.SchemeGuest, &cm.SchemeUser,
&cm.SchemeAdmin, &cm.TeamSchemeDefaultGuestRole, &cm.TeamSchemeDefaultUserRole,
&cm.TeamSchemeDefaultAdminRole, &cm.ChannelSchemeDefaultGuestRole,
&cm.ChannelSchemeDefaultUserRole, &cm.ChannelSchemeDefaultAdminRole,
)
if err != nil {
return nil, failure(err)
}
data = append(data, cm)
}
if err = rows.Err(); err != nil {
return nil, failure(err)
}
ids := data.ToMapStringString()
if allowFromCache {
allChannelMembersForUserCache.SetWithExpiry(cache_key, ids, ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_DURATION)
}
return ids, nil
}
func (s SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) {
allChannelMembersNotifyPropsForChannelCache.Remove(channelId)
if s.metrics != nil {
s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Remove by ChannelId")
}
}
type allChannelMemberNotifyProps struct {
UserId string
NotifyProps model.StringMap
}
func (s SqlChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId string, allowFromCache bool) (map[string]model.StringMap, *model.AppError) {
if allowFromCache {
var cacheItem map[string]model.StringMap
if err := allChannelMembersNotifyPropsForChannelCache.Get(channelId, &cacheItem); err == nil {
if s.metrics != nil {
s.metrics.IncrementMemCacheHitCounter("All Channel Members Notify Props for Channel")
}
return cacheItem, nil
}
}
if s.metrics != nil {
s.metrics.IncrementMemCacheMissCounter("All Channel Members Notify Props for Channel")
}
var data []allChannelMemberNotifyProps
_, err := s.GetReplica().Select(&data, `
SELECT UserId, NotifyProps
FROM ChannelMembers
WHERE ChannelId = :ChannelId`, map[string]interface{}{"ChannelId": channelId})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetAllChannelMembersPropsForChannel", "store.sql_channel.get_members.app_error", nil, "channelId="+channelId+", err="+err.Error(), http.StatusInternalServerError)
}
props := make(map[string]model.StringMap)
for i := range data {
props[data[i].UserId] = data[i].NotifyProps
}
allChannelMembersNotifyPropsForChannelCache.SetWithExpiry(channelId, props, ALL_CHANNEL_MEMBERS_NOTIFY_PROPS_FOR_CHANNEL_CACHE_DURATION)
return props, nil
}
func (s SqlChannelStore) InvalidateMemberCount(channelId string) {
}
func (s SqlChannelStore) GetMemberCountFromCache(channelId string) int64 {
count, _ := s.GetMemberCount(channelId, true)
return count
}
func (s SqlChannelStore) GetMemberCount(channelId string, allowFromCache bool) (int64, *model.AppError) {
count, err := s.GetReplica().SelectInt(`
SELECT
count(*)
FROM
ChannelMembers,
Users
WHERE
ChannelMembers.UserId = Users.Id
AND ChannelMembers.ChannelId = :ChannelId
AND Users.DeleteAt = 0`, map[string]interface{}{"ChannelId": channelId})
if err != nil {
return 0, model.NewAppError("SqlChannelStore.GetMemberCount", "store.sql_channel.get_member_count.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError)
}
return count, nil
}
// GetMemberCountsByGroup returns a slice of ChannelMemberCountByGroup for a given channel
// which contains the number of channel members for each group and optionally the number of unique timezones present for each group in the channel
func (s SqlChannelStore) GetMemberCountsByGroup(channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
selectStr := "GroupMembers.GroupId, COUNT(ChannelMembers.UserId) AS ChannelMemberCount"
if includeTimezones {
// Length of default timezone (len {"automaticTimezone":"","manualTimezone":"","useAutomaticTimezone":"true"})
defaultTimezoneLength := `74`
// Beginning and end of the value for the automatic and manual timezones respectively
autoTimezone := `LOCATE(':', Users.Timezone) + 2`
autoTimezoneEnd := `LOCATE(',', Users.Timezone) - LOCATE(':', Users.Timezone) - 3`
manualTimezone := `LOCATE(',', Users.Timezone) + 19`
manualTimezoneEnd := `LOCATE('useAutomaticTimezone', Users.Timezone) - 22 - LOCATE(',', Users.Timezone)`
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
autoTimezone = `POSITION(':' IN Users.Timezone) + 2`
autoTimezoneEnd = `POSITION(',' IN Users.Timezone) - POSITION(':' IN Users.Timezone) - 3`
manualTimezone = `POSITION(',' IN Users.Timezone) + 19`
manualTimezoneEnd = `POSITION('useAutomaticTimezone' IN Users.Timezone) - 22 - POSITION(',' IN Users.Timezone)`
}
selectStr = `
GroupMembers.GroupId,
COUNT(ChannelMembers.UserId) AS ChannelMemberCount,
COUNT(DISTINCT
(
CASE WHEN Timezone like '%"useAutomaticTimezone":"true"}' AND LENGTH(Timezone) > ` + defaultTimezoneLength + `
THEN
SUBSTRING(
Timezone
FROM ` + autoTimezone + `
FOR ` + autoTimezoneEnd + `
)
WHEN Timezone like '%"useAutomaticTimezone":"false"}' AND LENGTH(Timezone) > ` + defaultTimezoneLength + `
THEN
SUBSTRING(
Timezone
FROM ` + manualTimezone + `
FOR ` + manualTimezoneEnd + `
)
END
)
) AS ChannelMemberTimezonesCount
`
}
query := s.getQueryBuilder().
Select(selectStr).
From("ChannelMembers").
Join("GroupMembers ON GroupMembers.UserId = ChannelMembers.UserId")
if includeTimezones {
query = query.Join("Users ON Users.Id = GroupMembers.UserId")
}
query = query.Where(sq.Eq{"ChannelMembers.ChannelId": channelID}).GroupBy("GroupMembers.GroupId")
queryString, args, err := query.ToSql()
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetMemberCountsByGroup", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var data []*model.ChannelMemberCountByGroup
if _, err = s.GetReplica().Select(&data, queryString, args...); err != nil {
return nil, model.NewAppError("SqlChannelStore.GetMemberCountsByGroup", "store.sql_channel.get_member_count.app_error", nil, "channel_id="+channelID+", "+err.Error(), http.StatusInternalServerError)
}
return data, nil
}
func (s SqlChannelStore) InvalidatePinnedPostCount(channelId string) {
}
func (s SqlChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, *model.AppError) {
count, err := s.GetReplica().SelectInt(`
SELECT count(*)
FROM Posts
WHERE
IsPinned = true
AND ChannelId = :ChannelId
AND DeleteAt = 0`, map[string]interface{}{"ChannelId": channelId})
if err != nil {
return 0, model.NewAppError("SqlChannelStore.GetPinnedPostCount", "store.sql_channel.get_pinnedpost_count.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError)
}
return count, nil
}
func (s SqlChannelStore) InvalidateGuestCount(channelId string) {
}
func (s SqlChannelStore) GetGuestCount(channelId string, allowFromCache bool) (int64, *model.AppError) {
count, err := s.GetReplica().SelectInt(`
SELECT
count(*)
FROM
ChannelMembers,
Users
WHERE
ChannelMembers.UserId = Users.Id
AND ChannelMembers.ChannelId = :ChannelId
AND ChannelMembers.SchemeGuest = TRUE
AND Users.DeleteAt = 0`, map[string]interface{}{"ChannelId": channelId})
if err != nil {
return 0, model.NewAppError("SqlChannelStore.GetGuestCount", "store.sql_channel.get_member_count.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError)
}
return count, nil
}
func (s SqlChannelStore) RemoveMembers(channelId string, userIds []string) *model.AppError {
query := s.getQueryBuilder().
Delete("ChannelMembers").
Where(sq.Eq{"ChannelId": channelId}).
Where(sq.Eq{"UserId": userIds})
sql, args, err := query.ToSql()
if err != nil {
return model.NewAppError("SqlChannelStore.RemoveMember", "store.sql_channel.remove_member.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError)
}
_, err = s.GetMaster().Exec(sql, args...)
if err != nil {
return model.NewAppError("SqlChannelStore.RemoveMember", "store.sql_channel.remove_member.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError)
}
// cleanup sidebarchannels table if the user is no longer a member of that channel
sql, args, err = s.getQueryBuilder().
Delete("SidebarChannels").
Where(sq.And{
sq.Eq{"ChannelId": channelId},
sq.Eq{"UserId": userIds},
}).ToSql()
if err != nil {
return model.NewAppError("SqlChannelStore.RemoveMember", "store.sql_channel.remove_member.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError)
}
_, err = s.GetMaster().Exec(sql, args...)
if err != nil {
return model.NewAppError("SqlChannelStore.RemoveMember", "store.sql_channel.remove_member.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) RemoveMember(channelId string, userId string) *model.AppError {
return s.RemoveMembers(channelId, []string{userId})
}
func (s SqlChannelStore) RemoveAllDeactivatedMembers(channelId string) *model.AppError {
query := `
DELETE
FROM
ChannelMembers
WHERE
UserId IN (
SELECT
Id
FROM
Users
WHERE
Users.DeleteAt != 0
)
AND
ChannelMembers.ChannelId = :ChannelId
`
_, err := s.GetMaster().Exec(query, map[string]interface{}{"ChannelId": channelId})
if err != nil {
return model.NewAppError("SqlChannelStore.RemoveAllDeactivatedMembers", "store.sql_channel.remove_all_deactivated_members.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) PermanentDeleteMembersByUser(userId string) *model.AppError {
if _, err := s.GetMaster().Exec("DELETE FROM ChannelMembers WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}); err != nil {
return model.NewAppError("SqlChannelStore.ChannelPermanentDeleteMembersByUser", "store.sql_channel.permanent_delete_members_by_user.app_error", nil, "user_id="+userId+", "+err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, *model.AppError) {
keys, props := MapStringsToQueryParams(channelIds, "Channel")
props["UserId"] = userId
var lastPostAtTimes []struct {
Id string
LastPostAt int64
TotalMsgCount int64
}
query := `SELECT Id, LastPostAt, TotalMsgCount FROM Channels WHERE Id IN ` + keys
// TODO: use a CTE for mysql too when version 8 becomes the minimum supported version.
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
query = `WITH c AS ( ` + query + `),
updated AS (
UPDATE
ChannelMembers cm
SET
MentionCount = 0,
MsgCount = greatest(cm.MsgCount, c.TotalMsgCount),
LastViewedAt = greatest(cm.LastViewedAt, c.LastPostAt),
LastUpdateAt = greatest(cm.LastViewedAt, c.LastPostAt)
FROM c
WHERE cm.UserId = :UserId
AND c.Id=cm.ChannelId
)
SELECT Id, LastPostAt FROM c`
}
_, err := s.GetMaster().Select(&lastPostAtTimes, query, props)
if err != nil || len(lastPostAtTimes) == 0 {
status := http.StatusInternalServerError
var extra string
if err == nil {
status = http.StatusBadRequest
extra = "No channels found"
} else {
extra = err.Error()
}
return nil, model.NewAppError("SqlChannelStore.UpdateLastViewedAt",
"store.sql_channel.update_last_viewed_at.app_error",
nil,
"channel_ids="+strings.Join(channelIds, ",")+", user_id="+userId+", "+extra,
status)
}
times := map[string]int64{}
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
for _, t := range lastPostAtTimes {
times[t.Id] = t.LastPostAt
}
return times, nil
}
msgCountQuery := ""
lastViewedQuery := ""
for index, t := range lastPostAtTimes {
times[t.Id] = t.LastPostAt
props["msgCount"+strconv.Itoa(index)] = t.TotalMsgCount
msgCountQuery += fmt.Sprintf("WHEN :channelId%d THEN GREATEST(MsgCount, :msgCount%d) ", index, index)
props["lastViewed"+strconv.Itoa(index)] = t.LastPostAt
lastViewedQuery += fmt.Sprintf("WHEN :channelId%d THEN GREATEST(LastViewedAt, :lastViewed%d) ", index, index)
props["channelId"+strconv.Itoa(index)] = t.Id
}
updateQuery := `UPDATE
ChannelMembers
SET
MentionCount = 0,
MsgCount = CASE ChannelId ` + msgCountQuery + ` END,
LastViewedAt = CASE ChannelId ` + lastViewedQuery + ` END,
LastUpdateAt = LastViewedAt
WHERE
UserId = :UserId
AND ChannelId IN ` + keys
if _, err := s.GetMaster().Exec(updateQuery, props); err != nil {
return nil, model.NewAppError("SqlChannelStore.UpdateLastViewedAt", "store.sql_channel.update_last_viewed_at.app_error", nil, "channel_ids="+strings.Join(channelIds, ",")+", user_id="+userId+", "+err.Error(), http.StatusInternalServerError)
}
return times, nil
}
// CountPostsAfter returns the number of posts in the given channel created after but not including the given timestamp. If given a non-empty user ID, only counts posts made by that user.
func (s SqlChannelStore) CountPostsAfter(channelId string, timestamp int64, userId string) (int, *model.AppError) {
joinLeavePostTypes, params := MapStringsToQueryParams([]string{
// These types correspond to the ones checked by Post.IsJoinLeaveMessage
model.POST_JOIN_LEAVE,
model.POST_ADD_REMOVE,
model.POST_JOIN_CHANNEL,
model.POST_LEAVE_CHANNEL,
model.POST_JOIN_TEAM,
model.POST_LEAVE_TEAM,
model.POST_ADD_TO_CHANNEL,
model.POST_REMOVE_FROM_CHANNEL,
model.POST_ADD_TO_TEAM,
model.POST_REMOVE_FROM_TEAM,
}, "PostType")
query := `
SELECT count(*)
FROM Posts
WHERE
ChannelId = :ChannelId
AND CreateAt > :CreateAt
AND Type NOT IN ` + joinLeavePostTypes + `
AND DeleteAt = 0
`
params["ChannelId"] = channelId
params["CreateAt"] = timestamp
if userId != "" {
query += " AND UserId = :UserId"
params["UserId"] = userId
}
unread, err := s.GetReplica().SelectInt(query, params)
if err != nil {
return 0, model.NewAppError("SqlChannelStore.CountPostsAfter", "store.sql_channel.count_posts_since.app_error", nil, fmt.Sprintf("channel_id=%s, timestamp=%d, err=%s", channelId, timestamp, err), http.StatusInternalServerError)
}
return int(unread), nil
}
// UpdateLastViewedAtPost updates a ChannelMember as if the user last read the channel at the time of the given post.
// If the provided mentionCount is -1, the given post and all posts after it are considered to be mentions. Returns
// an updated model.ChannelUnreadAt that can be returned to the client.
func (s SqlChannelStore) UpdateLastViewedAtPost(unreadPost *model.Post, userID string, mentionCount int) (*model.ChannelUnreadAt, *model.AppError) {
unreadDate := unreadPost.CreateAt - 1
unread, appErr := s.CountPostsAfter(unreadPost.ChannelId, unreadDate, "")
if appErr != nil {
return nil, appErr
}
params := map[string]interface{}{
"mentions": mentionCount,
"unreadCount": unread,
"lastViewedAt": unreadDate,
"userId": userID,
"channelId": unreadPost.ChannelId,
"updatedAt": model.GetMillis(),
}
// msg count uses the value from channels to prevent counting on older channels where no. of messages can be high.
// we only count the unread which will be a lot less in 99% cases
setUnreadQuery := `
UPDATE
ChannelMembers
SET
MentionCount = :mentions,
MsgCount = (SELECT TotalMsgCount FROM Channels WHERE ID = :channelId) - :unreadCount,
LastViewedAt = :lastViewedAt,
LastUpdateAt = :updatedAt
WHERE
UserId = :userId
AND ChannelId = :channelId
`
_, err := s.GetMaster().Exec(setUnreadQuery, params)
if err != nil {
return nil, model.NewAppError("SqlChannelStore.UpdateLastViewedAtPost", "store.sql_channel.update_last_viewed_at_post.app_error", params, "Error setting channel "+unreadPost.ChannelId+" as unread: "+err.Error(), http.StatusInternalServerError)
}
chanUnreadQuery := `
SELECT
c.TeamId TeamId,
cm.UserId UserId,
cm.ChannelId ChannelId,
cm.MsgCount MsgCount,
cm.MentionCount MentionCount,
cm.LastViewedAt LastViewedAt,
cm.NotifyProps NotifyProps
FROM
ChannelMembers cm
LEFT JOIN Channels c ON c.Id=cm.ChannelId
WHERE
cm.UserId = :userId
AND cm.channelId = :channelId
AND c.DeleteAt = 0
`
result := &model.ChannelUnreadAt{}
if err = s.GetMaster().SelectOne(result, chanUnreadQuery, params); err != nil {
return nil, model.NewAppError("SqlChannelStore.UpdateLastViewedAtPost", "store.sql_channel.update_last_viewed_at_post.app_error", params, "Error retrieving unread status from channel "+unreadPost.ChannelId+", error was: "+err.Error(), http.StatusInternalServerError)
}
return result, nil
}
func (s SqlChannelStore) IncrementMentionCount(channelId string, userId string) *model.AppError {
_, err := s.GetMaster().Exec(
`UPDATE
ChannelMembers
SET
MentionCount = MentionCount + 1,
LastUpdateAt = :LastUpdateAt
WHERE
UserId = :UserId
AND ChannelId = :ChannelId`,
map[string]interface{}{"ChannelId": channelId, "UserId": userId, "LastUpdateAt": model.GetMillis()})
if err != nil {
return model.NewAppError("SqlChannelStore.IncrementMentionCount", "store.sql_channel.increment_mention_count.app_error", nil, "channel_id="+channelId+", user_id="+userId+", "+err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) GetAll(teamId string) ([]*model.Channel, *model.AppError) {
var data []*model.Channel
_, err := s.GetReplica().Select(&data, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Type != 'D' ORDER BY Name", map[string]interface{}{"TeamId": teamId})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetAll", "store.sql_channel.get_all.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError)
}
return data, nil
}
func (s SqlChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bool) ([]*model.Channel, *model.AppError) {
keys, params := MapStringsToQueryParams(channelIds, "Channel")
query := `SELECT * FROM Channels WHERE Id IN ` + keys + ` ORDER BY Name`
if !includeDeleted {
query = `SELECT * FROM Channels WHERE DeleteAt=0 AND Id IN ` + keys + ` ORDER BY Name`
}
var channels []*model.Channel
_, err := s.GetReplica().Select(&channels, query, params)
if err != nil {
mlog.Error("Query error getting channels by ids", mlog.Err(err))
return nil, model.NewAppError("SqlChannelStore.GetChannelsByIds", "store.sql_channel.get_channels_by_ids.app_error", nil, "", http.StatusInternalServerError)
}
return channels, nil
}
func (s SqlChannelStore) GetForPost(postId string) (*model.Channel, *model.AppError) {
channel := &model.Channel{}
if err := s.GetReplica().SelectOne(
channel,
`SELECT
Channels.*
FROM
Channels,
Posts
WHERE
Channels.Id = Posts.ChannelId
AND Posts.Id = :PostId`, map[string]interface{}{"PostId": postId}); err != nil {
return nil, model.NewAppError("SqlChannelStore.GetForPost", "store.sql_channel.get_for_post.app_error", nil, "postId="+postId+", err="+err.Error(), http.StatusInternalServerError)
}
return channel, nil
}
func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) (int64, *model.AppError) {
query := "SELECT COUNT(Id) AS Value FROM Channels WHERE Type = :ChannelType"
if len(teamId) > 0 {
query += " AND TeamId = :TeamId"
}
value, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
if err != nil {
return int64(0), model.NewAppError("SqlChannelStore.AnalyticsTypeCount", "store.sql_channel.analytics_type_count.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return value, nil
}
func (s SqlChannelStore) AnalyticsDeletedTypeCount(teamId string, channelType string) (int64, *model.AppError) {
query := "SELECT COUNT(Id) AS Value FROM Channels WHERE Type = :ChannelType AND DeleteAt > 0"
if len(teamId) > 0 {
query += " AND TeamId = :TeamId"
}
v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
if err != nil {
return 0, model.NewAppError("SqlChannelStore.AnalyticsDeletedTypeCount", "store.sql_channel.analytics_deleted_type_count.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return v, nil
}
func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) (*model.ChannelMembers, *model.AppError) {
var dbMembers channelMemberWithSchemeRolesList
_, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.UserId = :UserId AND (Teams.Id = :TeamId OR Teams.Id = '' OR Teams.Id IS NULL)", map[string]interface{}{"TeamId": teamId, "UserId": userId})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetMembersForUser", "store.sql_channel.get_members.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error(), http.StatusInternalServerError)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetMembersForUserWithPagination(teamId, userId string, page, perPage int) (*model.ChannelMembers, *model.AppError) {
var dbMembers channelMemberWithSchemeRolesList
offset := page * perPage
_, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.UserId = :UserId Limit :Limit Offset :Offset", map[string]interface{}{"TeamId": teamId, "UserId": userId, "Limit": perPage, "Offset": offset})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetMembersForUserWithPagination", "store.sql_channel.get_members.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error(), http.StatusInternalServerError)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) AutocompleteInTeam(teamId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) {
deleteFilter := "AND Channels.DeleteAt = 0"
if includeDeleted {
deleteFilter = ""
}
queryFormat := `
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels c ON (c.Id = Channels.Id)
WHERE
Channels.TeamId = :TeamId
` + deleteFilter + `
%v
LIMIT ` + strconv.Itoa(model.CHANNEL_SEARCH_DEFAULT_LIMIT)
var channels model.ChannelList
if likeClause, likeTerm := s.buildLIKEClause(term, "c.Name, c.DisplayName, c.Purpose"); likeClause == "" {
if _, err := s.GetReplica().Select(&channels, fmt.Sprintf(queryFormat, ""), map[string]interface{}{"TeamId": teamId}); err != nil {
return nil, model.NewAppError("SqlChannelStore.AutocompleteInTeam", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
} else {
// Using a UNION results in index_merge and fulltext queries and is much faster than the ref
// query you would get using an OR of the LIKE and full-text clauses.
fulltextClause, fulltextTerm := s.buildFulltextClause(term, "c.Name, c.DisplayName, c.Purpose")
likeQuery := fmt.Sprintf(queryFormat, "AND "+likeClause)
fulltextQuery := fmt.Sprintf(queryFormat, "AND "+fulltextClause)
query := fmt.Sprintf("(%v) UNION (%v) LIMIT 50", likeQuery, fulltextQuery)
if _, err := s.GetReplica().Select(&channels, query, map[string]interface{}{"TeamId": teamId, "LikeTerm": likeTerm, "FulltextTerm": fulltextTerm}); err != nil {
return nil, model.NewAppError("SqlChannelStore.AutocompleteInTeam", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
}
sort.Slice(channels, func(a, b int) bool {
return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName)
})
return &channels, nil
}
func (s SqlChannelStore) AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) {
deleteFilter := "AND DeleteAt = 0"
if includeDeleted {
deleteFilter = ""
}
queryFormat := `
SELECT
C.*
FROM
Channels AS C
JOIN
ChannelMembers AS CM ON CM.ChannelId = C.Id
WHERE
(C.TeamId = :TeamId OR (C.TeamId = '' AND C.Type = 'G'))
AND CM.UserId = :UserId
` + deleteFilter + `
%v
LIMIT 50`
var channels model.ChannelList
if likeClause, likeTerm := s.buildLIKEClause(term, "Name, DisplayName, Purpose"); likeClause == "" {
if _, err := s.GetReplica().Select(&channels, fmt.Sprintf(queryFormat, ""), map[string]interface{}{"TeamId": teamId, "UserId": userId}); err != nil {
return nil, model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
} else {
// Using a UNION results in index_merge and fulltext queries and is much faster than the ref
// query you would get using an OR of the LIKE and full-text clauses.
fulltextClause, fulltextTerm := s.buildFulltextClause(term, "Name, DisplayName, Purpose")
likeQuery := fmt.Sprintf(queryFormat, "AND "+likeClause)
fulltextQuery := fmt.Sprintf(queryFormat, "AND "+fulltextClause)
query := fmt.Sprintf("(%v) UNION (%v) LIMIT 50", likeQuery, fulltextQuery)
if _, err := s.GetReplica().Select(&channels, query, map[string]interface{}{"TeamId": teamId, "UserId": userId, "LikeTerm": likeTerm, "FulltextTerm": fulltextTerm}); err != nil {
return nil, model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
}
directChannels, err := s.autocompleteInTeamForSearchDirectMessages(userId, term)
if err != nil {
return nil, err
}
channels = append(channels, directChannels...)
sort.Slice(channels, func(a, b int) bool {
return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName)
})
return &channels, nil
}
func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userId string, term string) ([]*model.Channel, *model.AppError) {
queryFormat := `
SELECT
C.*,
OtherUsers.Username as DisplayName
FROM
Channels AS C
JOIN
ChannelMembers AS CM ON CM.ChannelId = C.Id
INNER JOIN (
SELECT
ICM.ChannelId AS ChannelId, IU.Username AS Username
FROM
Users as IU
JOIN
ChannelMembers AS ICM ON ICM.UserId = IU.Id
WHERE
IU.Id != :UserId
%v
) AS OtherUsers ON OtherUsers.ChannelId = C.Id
WHERE
C.Type = 'D'
AND CM.UserId = :UserId
LIMIT 50`
var channels model.ChannelList
if likeClause, likeTerm := s.buildLIKEClause(term, "IU.Username, IU.Nickname"); likeClause == "" {
if _, err := s.GetReplica().Select(&channels, fmt.Sprintf(queryFormat, ""), map[string]interface{}{"UserId": userId}); err != nil {
return nil, model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
} else {
query := fmt.Sprintf(queryFormat, "AND "+likeClause)
if _, err := s.GetReplica().Select(&channels, query, map[string]interface{}{"UserId": userId, "LikeTerm": likeTerm}); err != nil {
return nil, model.NewAppError("SqlChannelStore.AutocompleteInTeamForSearch", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
}
return channels, nil
}
func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) {
deleteFilter := "AND c.DeleteAt = 0"
if includeDeleted {
deleteFilter = ""
}
return s.performSearch(`
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels c ON (c.Id = Channels.Id)
WHERE
c.TeamId = :TeamId
`+deleteFilter+`
SEARCH_CLAUSE
ORDER BY c.DisplayName
LIMIT 100
`, term, map[string]interface{}{
"TeamId": teamId,
})
}
func (s SqlChannelStore) SearchArchivedInTeam(teamId string, term string, userId string) (*model.ChannelList, *model.AppError) {
publicChannels, publicErr := s.performSearch(`
SELECT
Channels.*
FROM
Channels
JOIN
Channels c ON (c.Id = Channels.Id)
WHERE
c.TeamId = :TeamId
SEARCH_CLAUSE
AND c.DeleteAt != 0
AND c.Type != 'P'
ORDER BY c.DisplayName
LIMIT 100
`, term, map[string]interface{}{
"TeamId": teamId,
"UserId": userId,
})
privateChannels, privateErr := s.performSearch(`
SELECT
Channels.*
FROM
Channels
JOIN
Channels c ON (c.Id = Channels.Id)
WHERE
c.TeamId = :TeamId
SEARCH_CLAUSE
AND c.DeleteAt != 0
AND c.Type = 'P'
AND c.Id IN (SELECT ChannelId FROM ChannelMembers WHERE UserId = :UserId)
ORDER BY c.DisplayName
LIMIT 100
`, term, map[string]interface{}{
"TeamId": teamId,
"UserId": userId,
})
output := *publicChannels
output = append(output, *privateChannels...)
outputErr := publicErr
if privateErr != nil {
outputErr = privateErr
}
return &output, outputErr
}
func (s SqlChannelStore) SearchForUserInTeam(userId string, teamId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) {
deleteFilter := "AND c.DeleteAt = 0"
if includeDeleted {
deleteFilter = ""
}
return s.performSearch(`
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels c ON (c.Id = Channels.Id)
JOIN
ChannelMembers cm ON (c.Id = cm.ChannelId)
WHERE
c.TeamId = :TeamId
AND
cm.UserId = :UserId
`+deleteFilter+`
SEARCH_CLAUSE
ORDER BY c.DisplayName
LIMIT 100
`, term, map[string]interface{}{
"TeamId": teamId,
"UserId": userId,
})
}
func (s SqlChannelStore) channelSearchQuery(term string, opts store.ChannelSearchOpts, countQuery bool) sq.SelectBuilder {
var limit int
if opts.PerPage != nil {
limit = *opts.PerPage
} else {
limit = 100
}
var selectStr string
if countQuery {
selectStr = "count(*)"
} else {
selectStr = "c.*, t.DisplayName AS TeamDisplayName, t.Name AS TeamName, t.UpdateAt as TeamUpdateAt"
}
query := s.getQueryBuilder().
Select(selectStr).
From("Channels AS c").
Join("Teams AS t ON t.Id = c.TeamId")
// don't bother ordering or limiting if we're just getting the count
if !countQuery {
query = query.
OrderBy("c.DisplayName, t.DisplayName").
Limit(uint64(limit))
}
if opts.Deleted {
query = query.Where(sq.NotEq{"c.DeleteAt": int(0)})
} else if !opts.IncludeDeleted {
query = query.Where(sq.Eq{"c.DeleteAt": int(0)})
}
if opts.IsPaginated() && !countQuery {
query = query.Offset(uint64(*opts.Page * *opts.PerPage))
}
likeClause, likeTerm := s.buildLIKEClause(term, "c.Name, c.DisplayName, c.Purpose")
if likeTerm != "" {
likeClause = strings.ReplaceAll(likeClause, ":LikeTerm", "?")
fulltextClause, fulltextTerm := s.buildFulltextClause(term, "c.Name, c.DisplayName, c.Purpose")
fulltextClause = strings.ReplaceAll(fulltextClause, ":FulltextTerm", "?")
query = query.Where(sq.Or{
sq.Expr(likeClause, likeTerm, likeTerm, likeTerm), // Keep the number of likeTerms same as the number
// of columns (c.Name, c.DisplayName, c.Purpose)
sq.Expr(fulltextClause, fulltextTerm),
})
}
if len(opts.ExcludeChannelNames) > 0 {
query = query.Where(sq.NotEq{"c.Name": opts.ExcludeChannelNames})
}
if len(opts.NotAssociatedToGroup) > 0 {
query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup)
}
if len(opts.TeamIds) > 0 {
query = query.Where(sq.Eq{"c.TeamId": opts.TeamIds})
}
if opts.GroupConstrained {
query = query.Where(sq.Eq{"c.GroupConstrained": true})
} else if opts.ExcludeGroupConstrained {
query = query.Where(sq.Or{
sq.NotEq{"c.GroupConstrained": true},
sq.Eq{"c.GroupConstrained": nil},
})
}
if opts.Public && !opts.Private {
query = query.Where(sq.Eq{"c.Type": model.CHANNEL_OPEN})
} else if opts.Private && !opts.Public {
query = query.Where(sq.Eq{"c.Type": model.CHANNEL_PRIVATE})
} else {
query = query.Where(sq.Or{
sq.Eq{"c.Type": model.CHANNEL_OPEN},
sq.Eq{"c.Type": model.CHANNEL_PRIVATE},
})
}
return query
}
func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) (*model.ChannelListWithTeamData, int64, *model.AppError) {
queryString, args, err := s.channelSearchQuery(term, opts, false).ToSql()
if err != nil {
return nil, 0, model.NewAppError("SqlChannelStore.SearchAllChannels", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var channels model.ChannelListWithTeamData
if _, err = s.GetReplica().Select(&channels, queryString, args...); err != nil {
return nil, 0, model.NewAppError("SqlChannelStore.Search", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
var totalCount int64
// only query a 2nd time for the count if the results are being requested paginated.
if opts.IsPaginated() {
queryString, args, err = s.channelSearchQuery(term, opts, true).ToSql()
if err != nil {
return nil, 0, model.NewAppError("SqlChannelStore.SearchAllChannels", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if totalCount, err = s.GetReplica().SelectInt(queryString, args...); err != nil {
return nil, 0, model.NewAppError("SqlChannelStore.Search", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
} else {
totalCount = int64(len(channels))
}
return &channels, totalCount, nil
}
func (s SqlChannelStore) SearchMore(userId string, teamId string, term string) (*model.ChannelList, *model.AppError) {
return s.performSearch(`
SELECT
Channels.*
FROM
Channels
JOIN
PublicChannels c ON (c.Id = Channels.Id)
WHERE
c.TeamId = :TeamId
AND c.DeleteAt = 0
AND c.Id NOT IN (
SELECT
c.Id
FROM
PublicChannels c
JOIN
ChannelMembers cm ON (cm.ChannelId = c.Id)
WHERE
c.TeamId = :TeamId
AND cm.UserId = :UserId
AND c.DeleteAt = 0
)
SEARCH_CLAUSE
ORDER BY c.DisplayName
LIMIT 100
`, term, map[string]interface{}{
"TeamId": teamId,
"UserId": userId,
})
}
func (s SqlChannelStore) buildLIKEClause(term string, searchColumns string) (likeClause, likeTerm string) {
likeTerm = sanitizeSearchTerm(term, "*")
if likeTerm == "" {
return
}
// Prepare the LIKE portion of the query.
var searchFields []string
for _, field := range strings.Split(searchColumns, ", ") {
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
searchFields = append(searchFields, fmt.Sprintf("lower(%s) LIKE lower(%s) escape '*'", field, ":LikeTerm"))
} else {
searchFields = append(searchFields, fmt.Sprintf("%s LIKE %s escape '*'", field, ":LikeTerm"))
}
}
likeClause = fmt.Sprintf("(%s)", strings.Join(searchFields, " OR "))
likeTerm = wildcardSearchTerm(likeTerm)
return
}
func (s SqlChannelStore) buildFulltextClause(term string, searchColumns string) (fulltextClause, fulltextTerm string) {
// Copy the terms as we will need to prepare them differently for each search type.
fulltextTerm = term
// These chars must be treated as spaces in the fulltext query.
for _, c := range spaceFulltextSearchChar {
fulltextTerm = strings.Replace(fulltextTerm, c, " ", -1)
}
// Prepare the FULLTEXT portion of the query.
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
fulltextTerm = strings.Replace(fulltextTerm, "|", "", -1)
splitTerm := strings.Fields(fulltextTerm)
for i, t := range strings.Fields(fulltextTerm) {
if i == len(splitTerm)-1 {
splitTerm[i] = t + ":*"
} else {
splitTerm[i] = t + ":* &"
}
}
fulltextTerm = strings.Join(splitTerm, " ")
fulltextClause = fmt.Sprintf("((to_tsvector('english', %s)) @@ to_tsquery('english', :FulltextTerm))", convertMySQLFullTextColumnsToPostgres(searchColumns))
} else if s.DriverName() == model.DATABASE_DRIVER_MYSQL {
splitTerm := strings.Fields(fulltextTerm)
for i, t := range strings.Fields(fulltextTerm) {
splitTerm[i] = "+" + t + "*"
}
fulltextTerm = strings.Join(splitTerm, " ")
fulltextClause = fmt.Sprintf("MATCH(%s) AGAINST (:FulltextTerm IN BOOLEAN MODE)", searchColumns)
}
return
}
func (s SqlChannelStore) performSearch(searchQuery string, term string, parameters map[string]interface{}) (*model.ChannelList, *model.AppError) {
likeClause, likeTerm := s.buildLIKEClause(term, "c.Name, c.DisplayName, c.Purpose")
if likeTerm == "" {
// If the likeTerm is empty after preparing, then don't bother searching.
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1)
} else {
parameters["LikeTerm"] = likeTerm
fulltextClause, fulltextTerm := s.buildFulltextClause(term, "c.Name, c.DisplayName, c.Purpose")
parameters["FulltextTerm"] = fulltextTerm
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "AND ("+likeClause+" OR "+fulltextClause+")", 1)
}
var channels model.ChannelList
if _, err := s.GetReplica().Select(&channels, searchQuery, parameters); err != nil {
return nil, model.NewAppError("SqlChannelStore.Search", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError)
}
return &channels, nil
}
func (s SqlChannelStore) getSearchGroupChannelsQuery(userId, term string, isPostgreSQL bool) (string, map[string]interface{}) {
var query, baseLikeClause string
if isPostgreSQL {
baseLikeClause = "ARRAY_TO_STRING(ARRAY_AGG(u.Username), ', ') LIKE %s"
query = `
SELECT
*
FROM
Channels
WHERE
Id IN (
SELECT
cc.Id
FROM (
SELECT
c.Id
FROM
Channels c
JOIN
ChannelMembers cm on c.Id = cm.ChannelId
JOIN
Users u on u.Id = cm.UserId
WHERE
c.Type = 'G'
AND
u.Id = :UserId
GROUP BY
c.Id
) cc
JOIN
ChannelMembers cm on cc.Id = cm.ChannelId
JOIN
Users u on u.Id = cm.UserId
GROUP BY
cc.Id
HAVING
%s
LIMIT
` + strconv.Itoa(model.CHANNEL_SEARCH_DEFAULT_LIMIT) + `
)`
} else {
baseLikeClause = "GROUP_CONCAT(u.Username SEPARATOR ', ') LIKE %s"
query = `
SELECT
cc.*
FROM (
SELECT
c.*
FROM
Channels c
JOIN
ChannelMembers cm on c.Id = cm.ChannelId
JOIN
Users u on u.Id = cm.UserId
WHERE
c.Type = 'G'
AND
u.Id = :UserId
GROUP BY
c.Id
) cc
JOIN
ChannelMembers cm on cc.Id = cm.ChannelId
JOIN
Users u on u.Id = cm.UserId
GROUP BY
cc.Id
HAVING
%s
LIMIT
` + strconv.Itoa(model.CHANNEL_SEARCH_DEFAULT_LIMIT)
}
var likeClauses []string
args := map[string]interface{}{"UserId": userId}
terms := strings.Split(strings.ToLower(strings.Trim(term, " ")), " ")
for idx, term := range terms {
argName := fmt.Sprintf("Term%v", idx)
term = sanitizeSearchTerm(term, "\\")
likeClauses = append(likeClauses, fmt.Sprintf(baseLikeClause, ":"+argName))
args[argName] = "%" + term + "%"
}
query = fmt.Sprintf(query, strings.Join(likeClauses, " AND "))
return query, args
}
func (s SqlChannelStore) SearchGroupChannels(userId, term string) (*model.ChannelList, *model.AppError) {
isPostgreSQL := s.DriverName() == model.DATABASE_DRIVER_POSTGRES
queryString, args := s.getSearchGroupChannelsQuery(userId, term, isPostgreSQL)
var groupChannels model.ChannelList
if _, err := s.GetReplica().Select(&groupChannels, queryString, args); err != nil {
return nil, model.NewAppError("SqlChannelStore.SearchGroupChannels", "store.sql_channel.search_group_channels.app_error", nil, "userId="+userId+", term="+term+", err="+err.Error(), http.StatusInternalServerError)
}
return &groupChannels, nil
}
func (s SqlChannelStore) GetMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, *model.AppError) {
var dbMembers channelMemberWithSchemeRolesList
props := make(map[string]interface{})
idQuery := ""
for index, userId := range userIds {
if len(idQuery) > 0 {
idQuery += ", "
}
props["userId"+strconv.Itoa(index)] = userId
idQuery += ":userId" + strconv.Itoa(index)
}
props["ChannelId"] = channelId
if _, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId IN ("+idQuery+")", props); err != nil {
return nil, model.NewAppError("SqlChannelStore.GetMembersByIds", "store.sql_channel.get_members_by_ids.app_error", nil, "channelId="+channelId+" "+err.Error(), http.StatusInternalServerError)
}
return dbMembers.ToModel(), nil
}
func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit int) (model.ChannelList, *model.AppError) {
var channels model.ChannelList
_, err := s.GetReplica().Select(&channels, "SELECT * FROM Channels WHERE SchemeId = :SchemeId ORDER BY DisplayName LIMIT :Limit OFFSET :Offset", map[string]interface{}{"SchemeId": schemeId, "Offset": offset, "Limit": limit})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetChannelsByScheme", "store.sql_channel.get_by_scheme.app_error", nil, "schemeId="+schemeId+" "+err.Error(), http.StatusInternalServerError)
}
return channels, nil
}
// This function does the Advanced Permissions Phase 2 migration for ChannelMember objects. It performs the migration
// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
func (s SqlChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId string) (map[string]string, *model.AppError) {
var transaction *gorp.Transaction
var err error
if transaction, err = s.GetMaster().Begin(); err != nil {
return nil, model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
var channelMembers []channelMember
if _, err := transaction.Select(&channelMembers, "SELECT * from ChannelMembers WHERE (ChannelId, UserId) > (:FromChannelId, :FromUserId) ORDER BY ChannelId, UserId LIMIT 100", map[string]interface{}{"FromChannelId": fromChannelId, "FromUserId": fromUserId}); err != nil {
return nil, model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.select.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(channelMembers) == 0 {
// No more channel members in query result means that the migration has finished.
return nil, nil
}
for i := range channelMembers {
member := channelMembers[i]
roles := strings.Fields(member.Roles)
var newRoles []string
if !member.SchemeAdmin.Valid {
member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeUser.Valid {
member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
}
if !member.SchemeGuest.Valid {
member.SchemeGuest = sql.NullBool{Bool: false, Valid: true}
}
for _, role := range roles {
if role == model.CHANNEL_ADMIN_ROLE_ID {
member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
} else if role == model.CHANNEL_USER_ROLE_ID {
member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
} else if role == model.CHANNEL_GUEST_ROLE_ID {
member.SchemeGuest = sql.NullBool{Bool: true, Valid: true}
} else {
newRoles = append(newRoles, role)
}
}
member.Roles = strings.Join(newRoles, " ")
if _, err := transaction.Update(&member); err != nil {
return nil, model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.update.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
if err := transaction.Commit(); err != nil {
return nil, model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
data := make(map[string]string)
data["ChannelId"] = channelMembers[len(channelMembers)-1].ChannelId
data["UserId"] = channelMembers[len(channelMembers)-1].UserId
return data, nil
}
func (s SqlChannelStore) ResetAllChannelSchemes() *model.AppError {
transaction, err := s.GetMaster().Begin()
if err != nil {
return model.NewAppError("SqlChannelStore.ResetAllChannelSchemes", "store.sql_channel.reset_all_channel_schemes.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
resetErr := s.resetAllChannelSchemesT(transaction)
if resetErr != nil {
return resetErr
}
if err := transaction.Commit(); err != nil {
return model.NewAppError("SqlChannelStore.ResetAllChannelSchemes", "store.sql_channel.reset_all_channel_schemes.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) resetAllChannelSchemesT(transaction *gorp.Transaction) *model.AppError {
if _, err := transaction.Exec("UPDATE Channels SET SchemeId=''"); err != nil {
return model.NewAppError("SqlChannelStore.ResetAllChannelSchemes", "store.sql_channel.reset_all_channel_schemes.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) ClearAllCustomRoleAssignments() *model.AppError {
builtInRoles := model.MakeDefaultRoles()
lastUserId := strings.Repeat("0", 26)
lastChannelId := strings.Repeat("0", 26)
for {
var transaction *gorp.Transaction
var err error
if transaction, err = s.GetMaster().Begin(); err != nil {
return model.NewAppError("SqlChannelStore.ClearAllCustomRoleAssignments", "store.sql_channel.clear_all_custom_role_assignments.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var channelMembers []*channelMember
if _, err := transaction.Select(&channelMembers, "SELECT * from ChannelMembers WHERE (ChannelId, UserId) > (:ChannelId, :UserId) ORDER BY ChannelId, UserId LIMIT 1000", map[string]interface{}{"ChannelId": lastChannelId, "UserId": lastUserId}); err != nil {
finalizeTransaction(transaction)
return model.NewAppError("SqlChannelStore.ClearAllCustomRoleAssignments", "store.sql_channel.clear_all_custom_role_assignments.select.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(channelMembers) == 0 {
finalizeTransaction(transaction)
break
}
for _, member := range channelMembers {
lastUserId = member.UserId
lastChannelId = member.ChannelId
var newRoles []string
for _, role := range strings.Fields(member.Roles) {
for name := range builtInRoles {
if name == role {
newRoles = append(newRoles, role)
break
}
}
}
newRolesString := strings.Join(newRoles, " ")
if newRolesString != member.Roles {
if _, err := transaction.Exec("UPDATE ChannelMembers SET Roles = :Roles WHERE UserId = :UserId AND ChannelId = :ChannelId", map[string]interface{}{"Roles": newRolesString, "ChannelId": member.ChannelId, "UserId": member.UserId}); err != nil {
finalizeTransaction(transaction)
return model.NewAppError("SqlChannelStore.ClearAllCustomRoleAssignments", "store.sql_channel.clear_all_custom_role_assignments.update.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
}
if err := transaction.Commit(); err != nil {
finalizeTransaction(transaction)
return model.NewAppError("SqlChannelStore.ClearAllCustomRoleAssignments", "store.sql_channel.clear_all_custom_role_assignments.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return nil
}
func (s SqlChannelStore) GetAllChannelsForExportAfter(limit int, afterId string) ([]*model.ChannelForExport, *model.AppError) {
var channels []*model.ChannelForExport
if _, err := s.GetReplica().Select(&channels, `
SELECT
Channels.*,
Teams.Name as TeamName,
Schemes.Name as SchemeName
FROM Channels
INNER JOIN
Teams ON Channels.TeamId = Teams.Id
LEFT JOIN
Schemes ON Channels.SchemeId = Schemes.Id
WHERE
Channels.Id > :AfterId
AND Channels.Type IN ('O', 'P')
ORDER BY
Id
LIMIT :Limit`,
map[string]interface{}{"AfterId": afterId, "Limit": limit}); err != nil {
return nil, model.NewAppError("SqlChannelStore.GetAllChannelsForExportAfter", "store.sql_channel.get_all.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return channels, nil
}
func (s SqlChannelStore) GetChannelMembersForExport(userId string, teamId string) ([]*model.ChannelMemberForExport, *model.AppError) {
var members []*model.ChannelMemberForExport
_, err := s.GetReplica().Select(&members, `
SELECT
ChannelMembers.ChannelId,
ChannelMembers.UserId,
ChannelMembers.Roles,
ChannelMembers.LastViewedAt,
ChannelMembers.MsgCount,
ChannelMembers.MentionCount,
ChannelMembers.NotifyProps,
ChannelMembers.LastUpdateAt,
ChannelMembers.SchemeUser,
ChannelMembers.SchemeAdmin,
(ChannelMembers.SchemeGuest IS NOT NULL AND ChannelMembers.SchemeGuest) as SchemeGuest,
Channels.Name as ChannelName
FROM
ChannelMembers
INNER JOIN
Channels ON ChannelMembers.ChannelId = Channels.Id
WHERE
ChannelMembers.UserId = :UserId
AND Channels.TeamId = :TeamId
AND Channels.DeleteAt = 0`,
map[string]interface{}{"TeamId": teamId, "UserId": userId})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetChannelMembersForExport", "store.sql_channel.get_members.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error(), http.StatusInternalServerError)
}
return members, nil
}
func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId string) ([]*model.DirectChannelForExport, *model.AppError) {
var directChannelsForExport []*model.DirectChannelForExport
query := s.getQueryBuilder().
Select("Channels.*").
From("Channels").
Where(sq.And{
sq.Gt{"Channels.Id": afterId},
sq.Eq{"Channels.DeleteAt": int(0)},
sq.Eq{"Channels.Type": []string{"D", "G"}},
}).
OrderBy("Channels.Id").
Limit(uint64(limit))
queryString, args, err := query.ToSql()
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if _, err = s.GetReplica().Select(&directChannelsForExport, queryString, args...); err != nil {
return nil, model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var channelIds []string
for _, channel := range directChannelsForExport {
channelIds = append(channelIds, channel.Id)
}
query = s.getQueryBuilder().
Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest").
From("ChannelMembers cm").
Join("Users u ON ( u.Id = cm.UserId )").
Where(sq.And{
sq.Eq{"cm.ChannelId": channelIds},
sq.Eq{"u.DeleteAt": int(0)},
})
queryString, args, err = query.ToSql()
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var channelMembers []*model.ChannelMemberForExport
if _, err := s.GetReplica().Select(&channelMembers, queryString, args...); err != nil {
return nil, model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
}
// Populate each channel with its members
dmChannelsMap := make(map[string]*model.DirectChannelForExport)
for _, channel := range directChannelsForExport {
channel.Members = &[]string{}
dmChannelsMap[channel.Id] = channel
}
for _, member := range channelMembers {
members := dmChannelsMap[member.ChannelId].Members
*members = append(*members, member.Username)
}
return directChannelsForExport, nil
}
func (s SqlChannelStore) GetChannelsBatchForIndexing(startTime, endTime int64, limit int) ([]*model.Channel, *model.AppError) {
query :=
`SELECT
*
FROM
Channels
WHERE
Type = 'O'
AND
CreateAt >= :StartTime
AND
CreateAt < :EndTime
ORDER BY
CreateAt
LIMIT
:NumChannels`
var channels []*model.Channel
_, err := s.GetSearchReplica().Select(&channels, query, map[string]interface{}{"StartTime": startTime, "EndTime": endTime, "NumChannels": limit})
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetChannelsBatchForIndexing", "store.sql_channel.get_channels_batch_for_indexing.get.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return channels, nil
}
func (s SqlChannelStore) UserBelongsToChannels(userId string, channelIds []string) (bool, *model.AppError) {
query := s.getQueryBuilder().
Select("Count(*)").
From("ChannelMembers").
Where(sq.And{
sq.Eq{"UserId": userId},
sq.Eq{"ChannelId": channelIds},
})
queryString, args, err := query.ToSql()
if err != nil {
return false, model.NewAppError("SqlChannelStore.UserBelongsToChannels", "store.sql_channel.user_belongs_to_channels.app_error", nil, err.Error(), http.StatusInternalServerError)
}
c, err := s.GetReplica().SelectInt(queryString, args...)
if err != nil {
return false, model.NewAppError("SqlChannelStore.UserBelongsToChannels", "store.sql_channel.user_belongs_to_channels.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return c > 0, nil
}
func (s SqlChannelStore) UpdateMembersRole(channelID string, userIDs []string) *model.AppError {
sql := fmt.Sprintf(`
UPDATE
ChannelMembers
SET
SchemeAdmin = CASE WHEN UserId IN ('%s') THEN
TRUE
ELSE
FALSE
END
WHERE
ChannelId = :ChannelId
AND (SchemeGuest = false OR SchemeGuest IS NULL)
`, strings.Join(userIDs, "', '"))
if _, err := s.GetMaster().Exec(sql, map[string]interface{}{"ChannelId": channelID}); err != nil {
return model.NewAppError("SqlChannelStore.UpdateMembersRole", "store.update_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) GroupSyncedChannelCount() (int64, *model.AppError) {
query := s.getQueryBuilder().Select("COUNT(*)").From("Channels").Where(sq.Eq{"GroupConstrained": true, "DeleteAt": 0})
sql, args, err := query.ToSql()
if err != nil {
return 0, model.NewAppError("SqlChannelStore.GroupSyncedChannelCount", "store.sql_group.app_error", nil, err.Error(), http.StatusInternalServerError)
}
count, err := s.GetReplica().SelectInt(sql, args...)
if err != nil {
return 0, model.NewAppError("SqlChannelStore.GroupSyncedChannelCount", "store.select_error", nil, err.Error(), http.StatusInternalServerError)
}
return count, nil
}
type sidebarCategoryForJoin struct {
model.SidebarCategory
ChannelId *string
}
func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
transaction, err := s.GetMaster().Begin()
if err != nil {
return nil, model.NewAppError("SqlChannelStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
categoriesWithOrder, appErr := s.getSidebarCategoriesT(transaction, userId, teamId)
if appErr != nil {
return nil, appErr
}
if len(categoriesWithOrder.Categories) < 1 {
return nil, model.NewAppError("SqlChannelStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError)
}
newOrder := categoriesWithOrder.Order
newCategoryId := model.NewId()
newCategorySortOrder := 0
/*
When a new category is created, it should be placed as follows:
1. If the Favorites category is first, the new category should be placed after it
2. Otherwise, the new category should be placed first.
*/
if categoriesWithOrder.Categories[0].Type == model.SidebarCategoryFavorites {
newOrder = append([]string{newOrder[0], newCategoryId}, newOrder[1:]...)
newCategorySortOrder = model.MinimalSidebarSortDistance
} else {
newOrder = append([]string{newCategoryId}, newOrder...)
}
category := &model.SidebarCategory{
DisplayName: newCategory.DisplayName,
Id: newCategoryId,
UserId: userId,
TeamId: teamId,
Sorting: model.SidebarCategorySortDefault,
SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list
Type: model.SidebarCategoryCustom,
}
if err = transaction.Insert(category); err != nil {
return nil, model.NewAppError("SqlPostStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(newCategory.Channels) > 0 {
channelIdsKeys, deleteParams := MapStringsToQueryParams(newCategory.Channels, "ChannelId")
deleteParams["UserId"] = userId
deleteParams["TeamId"] = teamId
// Remove any channels from their previous categories and add them to the new one
var deleteQuery string
if s.DriverName() == model.DATABASE_DRIVER_MYSQL {
deleteQuery = `
DELETE
SidebarChannels
FROM
SidebarChannels
JOIN
SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id
WHERE
SidebarChannels.UserId = :UserId
AND SidebarChannels.ChannelId IN ` + channelIdsKeys + `
AND SidebarCategories.TeamId = :TeamId`
} else {
deleteQuery = `
DELETE FROM
SidebarChannels
USING
SidebarCategories
WHERE
SidebarChannels.CategoryId = SidebarCategories.Id
AND SidebarChannels.UserId = :UserId
AND SidebarChannels.ChannelId IN ` + channelIdsKeys + `
AND SidebarCategories.TeamId = :TeamId`
}
_, err = transaction.Exec(deleteQuery, deleteParams)
if err != nil {
return nil, model.NewAppError("SqlPostStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var channels []interface{}
for i, channelID := range newCategory.Channels {
channels = append(channels, &model.SidebarChannel{
ChannelId: channelID,
CategoryId: newCategoryId,
SortOrder: int64(i * model.MinimalSidebarSortDistance),
UserId: userId,
})
}
if err = transaction.Insert(channels...); err != nil {
return nil, model.NewAppError("SqlPostStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
// now we re-order the categories according to the new order
if appErr := s.updateSidebarCategoryOrderT(transaction, userId, teamId, newOrder); appErr != nil {
return nil, appErr
}
if err = transaction.Commit(); err != nil {
return nil, model.NewAppError("SqlChannelStore.CreateSidebarCategory", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
// patch category to return proper sort order
category.SortOrder = int64(newCategorySortOrder)
result := &model.SidebarCategoryWithChannels{
SidebarCategory: *category,
Channels: newCategory.Channels,
}
return result, nil
}
func (s SqlChannelStore) completePopulatingCategoryChannels(category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
transaction, err := s.GetMaster().Begin()
if err != nil {
return nil, model.NewAppError("SqlChannelStore.completePopulatingCategoryChannels", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
result, appErr := s.completePopulatingCategoryChannelsT(transaction, category)
if appErr != nil {
return nil, appErr
}
if err = transaction.Commit(); err != nil {
return nil, model.NewAppError("SqlChannelStore.completePopulatingCategoryChannels", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return result, nil
}
func (s SqlChannelStore) completePopulatingCategoryChannelsT(transation *gorp.Transaction, category *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
if category.Type == model.SidebarCategoryCustom || category.Type == model.SidebarCategoryFavorites {
return category, nil
}
var channelTypeFilter sq.Sqlizer
if category.Type == model.SidebarCategoryDirectMessages {
// 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": []string{model.CHANNEL_DIRECT, model.CHANNEL_GROUP}}
} else if category.Type == model.SidebarCategoryChannels {
// 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{
sq.Eq{"Channels.Type": []string{model.CHANNEL_OPEN, model.CHANNEL_PRIVATE}},
sq.Eq{"Channels.TeamId": category.TeamId},
}
}
// A subquery that is true if the channel does not have a SidebarChannel entry for the current user on the current team
doesNotHaveSidebarChannel := sq.Select("1").
Prefix("NOT EXISTS (").
From("SidebarChannels").
Join("SidebarCategories on SidebarChannels.CategoryId=SidebarCategories.Id").
Where(sq.And{
sq.Expr("SidebarChannels.ChannelId = ChannelMembers.ChannelId"),
sq.Eq{"SidebarCategories.UserId": category.UserId},
sq.Eq{"SidebarCategories.TeamId": category.TeamId},
}).
Suffix(")")
var channels []string
sql, args, _ := s.getQueryBuilder().
Select("Id").
From("ChannelMembers").
LeftJoin("Channels ON Channels.Id=ChannelMembers.ChannelId").
Where(sq.And{
sq.Eq{"ChannelMembers.UserId": category.UserId},
channelTypeFilter,
sq.Eq{"Channels.DeleteAt": 0},
doesNotHaveSidebarChannel,
}).
OrderBy("DisplayName ASC").ToSql()
if _, err := transation.Select(&channels, sql, args...); err != nil {
return nil, model.NewAppError("SqlPostStore.completePopulatingCategoryChannelsT", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound)
}
category.Channels = append(channels, category.Channels...)
return category, nil
}
func (s SqlChannelStore) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) {
var categories []*sidebarCategoryForJoin
sql, args, _ := s.getQueryBuilder().
Select("SidebarCategories.*", "SidebarChannels.ChannelId").
From("SidebarCategories").
LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=SidebarCategories.Id").
Where(sq.Eq{"SidebarCategories.Id": categoryId}).
OrderBy("SidebarChannels.SortOrder ASC").ToSql()
if _, err := s.GetReplica().Select(&categories, sql, args...); err != nil {
return nil, model.NewAppError("SqlPostStore.GetSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound)
}
result := &model.SidebarCategoryWithChannels{
SidebarCategory: categories[0].SidebarCategory,
Channels: make([]string, 0),
}
for _, category := range categories {
if category.ChannelId != nil {
result.Channels = append(result.Channels, *category.ChannelId)
}
}
return s.completePopulatingCategoryChannels(result)
}
func (s SqlChannelStore) getSidebarCategoriesT(transaction *gorp.Transaction, userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) {
oc := model.OrderedSidebarCategories{
Categories: make(model.SidebarCategoriesWithChannels, 0),
Order: make([]string, 0),
}
var categories []*sidebarCategoryForJoin
sql, args, _ := s.getQueryBuilder().
Select("SidebarCategories.*", "SidebarChannels.ChannelId").
From("SidebarCategories").
LeftJoin("SidebarChannels ON SidebarChannels.CategoryId=Id").
Where(sq.And{
sq.Eq{"SidebarCategories.UserId": userId},
sq.Eq{"SidebarCategories.TeamId": teamId},
}).
OrderBy("SidebarCategories.SortOrder ASC, SidebarChannels.SortOrder ASC").ToSql()
if _, err := transaction.Select(&categories, sql, args...); err != nil {
return nil, model.NewAppError("SqlPostStore.GetSidebarCategories", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound)
}
for _, category := range categories {
var prevCategory *model.SidebarCategoryWithChannels
for _, existing := range oc.Categories {
if existing.Id == category.Id {
prevCategory = existing
break
}
}
if prevCategory == nil {
prevCategory = &model.SidebarCategoryWithChannels{
SidebarCategory: category.SidebarCategory,
Channels: make([]string, 0),
}
oc.Categories = append(oc.Categories, prevCategory)
oc.Order = append(oc.Order, category.Id)
}
if category.ChannelId != nil {
prevCategory.Channels = append(prevCategory.Channels, *category.ChannelId)
}
}
for _, category := range oc.Categories {
if _, err := s.completePopulatingCategoryChannelsT(transaction, category); err != nil {
return nil, err
}
}
return &oc, nil
}
func (s SqlChannelStore) GetSidebarCategories(userId, teamId string) (*model.OrderedSidebarCategories, *model.AppError) {
transaction, err := s.GetMaster().Begin()
if err != nil {
return nil, model.NewAppError("SqlChannelStore.GetSidebarCategories", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
oc, appErr := s.getSidebarCategoriesT(transaction, userId, teamId)
if appErr != nil {
return nil, appErr
}
if err = transaction.Commit(); err != nil {
return nil, model.NewAppError("SqlChannelStore.GetSidebarCategories", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return oc, nil
}
func (s SqlChannelStore) GetSidebarCategoryOrder(userId, teamId string) ([]string, *model.AppError) {
var ids []string
sql, args, _ := s.getQueryBuilder().
Select("Id").
From("SidebarCategories").
Where(sq.And{
sq.Eq{"UserId": userId},
sq.Eq{"TeamId": teamId},
}).
OrderBy("SidebarCategories.SortOrder ASC").ToSql()
if _, err := s.GetReplica().Select(&ids, sql, args...); err != nil {
return nil, model.NewAppError("SqlPostStore.GetSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusNotFound)
}
return ids, nil
}
func (s SqlChannelStore) updateSidebarCategoryOrderT(transaction *gorp.Transaction, userId, teamId string, categoryOrder []string) *model.AppError {
var newOrder []interface{}
runningOrder := 0
for _, categoryId := range categoryOrder {
newOrder = append(newOrder, &model.SidebarCategory{
Id: categoryId,
SortOrder: int64(runningOrder),
})
runningOrder += model.MinimalSidebarSortDistance
}
// There's a bug in gorp where UpdateColumns messes up the stored query for any other attempt to use .Update or
// .UpdateColumns on this table, so it's okay to use here as long as we don't use those methods for SidebarCategories
// anywhere else.
if _, err := transaction.UpdateColumns(func(col *gorp.ColumnMap) bool {
return col.ColumnName == "SortOrder"
}, newOrder...); err != nil {
return model.NewAppError("SqlPostStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) UpdateSidebarCategoryOrder(userId, teamId string, categoryOrder []string) *model.AppError {
transaction, err := s.GetMaster().Begin()
if err != nil {
return model.NewAppError("SqlChannelStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
// Ensure no invalid categories are included and that no categories are left out
existingOrder, appErr := s.GetSidebarCategoryOrder(userId, teamId)
if appErr != nil {
return appErr
}
if len(existingOrder) != len(categoryOrder) {
return model.NewAppError("SqlPostStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, "Cannot update category order, passed list of categories different size than in DB", http.StatusInternalServerError)
}
for _, originalCategoryId := range existingOrder {
found := false
for _, newCategoryId := range categoryOrder {
if newCategoryId == originalCategoryId {
found = true
break
}
}
if !found {
return model.NewAppError("SqlPostStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.app_error", nil, "Cannot update category order, passed list of categories contains unrecognized category IDs", http.StatusBadRequest)
}
}
if appErr := s.updateSidebarCategoryOrderT(transaction, userId, teamId, categoryOrder); appErr != nil {
return appErr
}
if err = transaction.Commit(); err != nil {
return model.NewAppError("SqlChannelStore.UpdateSidebarCategoryOrder", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
transaction, err := s.GetMaster().Begin()
if err != nil {
return nil, model.NewAppError("SqlChannelStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
updatedCategories := []*model.SidebarCategoryWithChannels{}
for _, category := range categories {
originalCategory, appErr := s.GetSidebarCategory(category.Id)
if appErr != nil {
return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, appErr.Error(), http.StatusInternalServerError)
}
// Copy category to avoid modifying an argument
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: category.SidebarCategory,
}
// Prevent any changes to read-only fields of SidebarCategories
updatedCategory.UserId = originalCategory.UserId
updatedCategory.TeamId = originalCategory.TeamId
updatedCategory.SortOrder = originalCategory.SortOrder
updatedCategory.Type = originalCategory.Type
if updatedCategory.Type != model.SidebarCategoryCustom {
updatedCategory.DisplayName = originalCategory.DisplayName
}
if category.Type != model.SidebarCategoryDirectMessages {
updatedCategory.Channels = make([]string, len(category.Channels))
copy(updatedCategory.Channels, category.Channels)
}
updateQuery, updateParams, _ := s.getQueryBuilder().
Update("SidebarCategories").
Set("DisplayName", updatedCategory.DisplayName).
Set("Sorting", updatedCategory.Sorting).
Where(sq.Eq{"Id": updatedCategory.Id}).ToSql()
if _, err = transaction.Exec(updateQuery, updateParams...); err != nil {
return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
// if we are updating DM category, it's order can't channel order cannot be changed.
if category.Type != model.SidebarCategoryDirectMessages {
// Remove any SidebarChannels entries that were either:
// - previously in this category (and any ones that are still in the category will be recreated below)
// - in another category and are being added to this category
sql, args, _ := s.getQueryBuilder().
Delete("SidebarChannels").
Where(
sq.And{
sq.Or{
sq.Eq{"ChannelId": originalCategory.Channels},
sq.Eq{"ChannelId": updatedCategory.Channels},
},
sq.Eq{"CategoryId": category.Id},
},
).ToSql()
if _, err = transaction.Exec(sql, args...); err != nil {
return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var channels []interface{}
runningOrder := 0
for _, channelID := range category.Channels {
channels = append(channels, &model.SidebarChannel{
ChannelId: channelID,
CategoryId: category.Id,
SortOrder: int64(runningOrder),
UserId: userId,
})
runningOrder += model.MinimalSidebarSortDistance
}
if err = transaction.Insert(channels...); err != nil {
return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
// Update the favorites preferences based on channels moving into or out of the Favorites category for compatibility
if category.Type == model.SidebarCategoryFavorites {
// Remove any old favorites
sql, args, _ := s.getQueryBuilder().Delete("Preferences").Where(
sq.Eq{
"UserId": userId,
"Name": originalCategory.Channels,
"Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL,
},
).ToSql()
if _, err = transaction.Exec(sql, args...); err != nil {
return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
// And then add the new ones
for _, channelID := range category.Channels {
// This breaks the PreferenceStore abstraction, but it should be safe to assume that everything is a SQL
// store in this package.
if err = s.Preference().(*SqlPreferenceStore).save(transaction, &model.Preference{
Name: channelID,
UserId: userId,
Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL,
Value: "true",
}); err != nil {
return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
} else {
// Remove any old favorites that might have been in this category
sql, args, _ := s.getQueryBuilder().Delete("Preferences").Where(
sq.Eq{
"UserId": userId,
"Name": category.Channels,
"Category": model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL,
},
).ToSql()
if _, err = transaction.Exec(sql, args...); err != nil {
return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
updatedCategories = append(updatedCategories, updatedCategory)
}
// Ensure Channels are populated for Channels/Direct Messages category if they change
for i, updatedCategory := range updatedCategories {
populated, err := s.completePopulatingCategoryChannelsT(transaction, updatedCategory)
if err != nil {
return nil, model.NewAppError("SqlPostStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
updatedCategories[i] = populated
}
if err = transaction.Commit(); err != nil {
return nil, model.NewAppError("SqlChannelStore.UpdateSidebarCategory", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return updatedCategories, nil
}
// UpdateSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync
// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side)
func (s SqlChannelStore) UpdateSidebarChannelsByPreferences(preferences *model.Preferences) error {
transaction, err := s.GetMaster().Begin()
if err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: begin_transaction")
}
defer finalizeTransaction(transaction)
for _, preference := range *preferences {
preference := preference
if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL {
continue
}
// if new preference is false - remove the channel from the appropriate sidebar category
if preference.Value == "false" {
if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT")
}
} else {
if err := s.addChannelToFavoritesCategoryT(transaction, &preference); err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: addChannelToFavoritesCategoryT")
}
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "UpdateSidebarChannelsByPreferences: commit_transaction")
}
return nil
}
func (s SqlChannelStore) removeSidebarEntriesForPreferenceT(transaction *gorp.Transaction, preference *model.Preference) error {
if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL {
return nil
}
// Delete any corresponding SidebarChannels entries in a Favorites category corresponding to this preference. This
// can't use the query builder because it uses DB-specific syntax
params := map[string]interface{}{
"UserId": preference.UserId,
"ChannelId": preference.Name,
"CategoryType": model.SidebarCategoryFavorites,
}
var query string
if s.DriverName() == model.DATABASE_DRIVER_MYSQL {
query = `
DELETE
SidebarChannels
FROM
SidebarChannels
JOIN
SidebarCategories ON SidebarChannels.CategoryId = SidebarCategories.Id
WHERE
SidebarChannels.UserId = :UserId
AND SidebarChannels.ChannelId = :ChannelId
AND SidebarCategories.Type = :CategoryType`
} else {
query = `
DELETE FROM
SidebarChannels
USING
SidebarCategories
WHERE
SidebarChannels.CategoryId = SidebarCategories.Id
AND SidebarChannels.UserId = :UserId
AND SidebarChannels.ChannelId = :ChannelId
AND SidebarCategories.Type = :CategoryType`
}
if _, err := transaction.Exec(query, params); err != nil {
return errors.Wrap(err, "Failed to remove sidebar entries for preference")
}
return nil
}
func (s SqlChannelStore) addChannelToFavoritesCategoryT(transaction *gorp.Transaction, preference *model.Preference) error {
if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL {
return nil
}
var channel *model.Channel
if obj, err := transaction.Get(&model.Channel{}, preference.Name); err != nil {
return errors.Wrapf(err, "Failed to get favorited channel with id=%s", preference.Name)
} else {
channel = obj.(*model.Channel)
}
// Get the IDs of the Favorites category/categories that the channel needs to be added to
builder := s.getQueryBuilder().
Select("SidebarCategories.Id").
From("SidebarCategories").
LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId and SidebarChannels.ChannelId = ?", preference.Name).
Where(sq.Eq{
"SidebarCategories.UserId": preference.UserId,
"Type": model.SidebarCategoryFavorites,
}).
Where("SidebarChannels.ChannelId is null")
if channel.TeamId != "" {
builder = builder.Where(sq.Eq{"TeamId": channel.TeamId})
}
idsQuery, idsParams, _ := builder.ToSql()
var categoryIds []string
if _, err := transaction.Select(&categoryIds, idsQuery, idsParams...); err != nil {
return errors.Wrap(err, "Failed to get Favorites sidebar categories")
}
if len(categoryIds) == 0 {
// The channel is already in the Favorites category/categories
return nil
}
// For each category ID, insert a row into SidebarChannels with the given channel ID and a SortOrder that's less than
// all existing SortOrders in the category so that the newly favorited channel comes first
insertQuery, insertParams, _ := s.getQueryBuilder().
Insert("SidebarChannels").
Columns(
"ChannelId",
"CategoryId",
"UserId",
"SortOrder",
).
Select(
sq.Select().
Column("? as ChannelId", preference.Name).
Column("SidebarCategories.Id as CategoryId").
Column("? as UserId", preference.UserId).
Column("COALESCE(MIN(SidebarChannels.SortOrder) - 10, 0) as SortOrder").
From("SidebarCategories").
LeftJoin("SidebarChannels on SidebarCategories.Id = SidebarChannels.CategoryId").
Where(sq.Eq{
"SidebarCategories.Id": categoryIds,
}).
GroupBy("SidebarCategories.Id")).ToSql()
if _, err := transaction.Exec(insertQuery, insertParams...); err != nil {
return errors.Wrap(err, "Failed to add sidebar entries for favorited channel")
}
return nil
}
// DeleteSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync
// At the moment, it's only handling Favorites and NOT DMs/GMs (those will be handled client side)
func (s SqlChannelStore) DeleteSidebarChannelsByPreferences(preferences *model.Preferences) error {
transaction, err := s.GetMaster().Begin()
if err != nil {
return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: begin_transaction")
}
defer finalizeTransaction(transaction)
for _, preference := range *preferences {
preference := preference
if preference.Category != model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL {
continue
}
if err := s.removeSidebarEntriesForPreferenceT(transaction, &preference); err != nil {
return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: removeSidebarEntriesForPreferenceT")
}
}
if err := transaction.Commit(); err != nil {
return errors.Wrap(err, "DeleteSidebarChannelsByPreferences: commit_transaction")
}
return nil
}
func (s SqlChannelStore) UpdateSidebarChannelCategoryOnMove(channel *model.Channel, newTeamId string) *model.AppError {
// if channel is being moved, remove it from the categories, since it's possible that there's no matching category in the new team
if _, err := s.GetMaster().Exec("DELETE FROM SidebarChannels WHERE ChannelId=:ChannelId", map[string]interface{}{"ChannelId": channel.Id}); err != nil {
return model.NewAppError("SqlChannelStore.UpdateSidebarChannelCategoryOnMove", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s SqlChannelStore) ClearSidebarOnTeamLeave(userId, teamId string) *model.AppError {
// if user leaves the team, clean his team related entries in sidebar channels and categories
params := map[string]interface{}{
"UserId": userId,
"TeamId": teamId,
}
var deleteQuery string
if s.DriverName() == model.DATABASE_DRIVER_MYSQL {
deleteQuery = "DELETE SidebarChannels FROM SidebarChannels LEFT JOIN SidebarCategories ON SidebarCategories.Id = SidebarChannels.CategoryId WHERE SidebarCategories.TeamId=:TeamId AND SidebarCategories.UserId=:UserId"
} else {
deleteQuery = "DELETE FROM SidebarChannels USING SidebarChannels AS chan LEFT OUTER JOIN SidebarCategories AS cat ON cat.Id = chan.CategoryId WHERE cat.UserId = :UserId AND cat.TeamId = :TeamId"
}
if _, err := s.GetMaster().Exec(deleteQuery, params); err != nil {
return model.NewAppError("SqlChannelStore.ClearSidebarOnTeamLeave", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if _, err := s.GetMaster().Exec("DELETE FROM SidebarCategories WHERE SidebarCategories.TeamId = :TeamId AND SidebarCategories.UserId = :UserId", params); err != nil {
return model.NewAppError("SqlChannelStore.ClearSidebarOnTeamLeave", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
// DeleteSidebarCategory removes a custom category and moves any channels into it into the Channels and Direct Messages
// categories respectively. Assumes that the provided user ID and team ID match the given category ID.
func (s SqlChannelStore) DeleteSidebarCategory(categoryId string) *model.AppError {
transaction, err := s.GetMaster().Begin()
if err != nil {
return model.NewAppError("SqlChannelStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer finalizeTransaction(transaction)
// Ensure that we're deleting a custom category
var category *model.SidebarCategory
if err = transaction.SelectOne(&category, "SELECT * FROM SidebarCategories WHERE Id = :Id", map[string]interface{}{"Id": categoryId}); err != nil {
return model.NewAppError("SqlPostStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if category.Type != model.SidebarCategoryCustom {
return model.NewAppError("SqlPostStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.delete_invalid.app_error", nil, "", http.StatusBadRequest)
}
// Delete the channels in the category
sql, args, _ := s.getQueryBuilder().
Delete("SidebarChannels").
Where(sq.Eq{"CategoryId": categoryId}).ToSql()
if _, err := transaction.Exec(sql, args...); err != nil {
return model.NewAppError("SqlPostStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
// Delete the category itself
sql, args, _ = s.getQueryBuilder().
Delete("SidebarCategories").
Where(sq.Eq{"Id": categoryId}).ToSql()
if _, err := transaction.Exec(sql, args...); err != nil {
return model.NewAppError("SqlChannelStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err := transaction.Commit(); err != nil {
return model.NewAppError("SqlChannelStore.DeleteSidebarCategory", "store.sql_channel.sidebar_categories.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}