From 937b6480d534b3051cadf4a892a9aa210bec579a Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Thu, 25 Oct 2018 13:25:27 -0400 Subject: MM-12342: merge the experimental channel store (#9681) * MM-12342: merge the experimental channel store * gofmt after upgrading to go 1.11 --- store/sqlstore/channel_store.go | 390 ++++++++++---- store/sqlstore/channel_store_experimental.go | 778 --------------------------- store/sqlstore/supplier.go | 9 +- store/store.go | 4 - store/storetest/channel_store.go | 105 ++-- store/storetest/mocks/ChannelStore.go | 38 -- 6 files changed, 325 insertions(+), 999 deletions(-) delete mode 100644 store/sqlstore/channel_store_experimental.go (limited to 'store') diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 63b9fbb42..5947a8f82 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/mattermost/gorp" + "github.com/pkg/errors" "github.com/mattermost/mattermost-server/einterfaces" "github.com/mattermost/mattermost-server/mlog" @@ -232,6 +233,17 @@ func (db allChannelMembers) ToMapStringString() map[string]string { 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 channelMemberCountsCache = utils.NewLru(CHANNEL_MEMBERS_COUNTS_CACHE_SIZE) var allChannelMembersForUserCache = utils.NewLru(ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE) var allChannelMembersNotifyPropsForChannelCache = utils.NewLru(ALL_CHANNEL_MEMBERS_NOTIFY_PROPS_FOR_CHANNEL_CACHE_SIZE) @@ -278,6 +290,15 @@ func NewSqlChannelStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) 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) } return s @@ -299,21 +320,112 @@ func (s SqlChannelStore) CreateIndexesIfNotExists() { 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") } +// 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 { - // See SqlChannelStoreExperimental + transaction, err := s.GetMaster().Begin() + if err != nil { + return err + } + + if _, err := transaction.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 + } + + if err := transaction.Commit(); err != nil { + return err + } + return nil } -func (s SqlChannelStore) DropPublicChannels() error { - // See SqlChannelStoreExperimental +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. -// -// @see ChannelStoreExperimental for how this update propagates to the PublicChannels table. func (s SqlChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) store.StoreChannel { return store.Do(func(result *store.StoreResult) { if channel.DeleteAt != 0 { @@ -338,6 +450,13 @@ func (s SqlChannelStore) Save(channel *model.Channel, maxChannelsPerTeam int64) return } + // Additionally propagate the write to the PublicChannels table. + if err := s.upsertPublicChannelT(transaction, result.Data.(*model.Channel)); err != nil { + transaction.Rollback() + result.Err = model.NewAppError("SqlChannelStore.Save", "store.sql_channel.save.upsert_public_channel.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + if err := transaction.Commit(); err != nil { result.Err = model.NewAppError("SqlChannelStore.Save", "store.sql_channel.save.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) return @@ -473,8 +592,6 @@ func (s SqlChannelStore) saveChannelT(transaction *gorp.Transaction, channel *mo } // Update writes the updated channel to the database. -// -// @see ChannelStoreExperimental for how this update propagates to the PublicChannels table. func (s SqlChannelStore) Update(channel *model.Channel) store.StoreChannel { return store.Do(func(result *store.StoreResult) { transaction, err := s.GetMaster().Begin() @@ -489,11 +606,19 @@ func (s SqlChannelStore) Update(channel *model.Channel) store.StoreChannel { return } + // Additionally propagate the write to the PublicChannels table. + if err := s.upsertPublicChannelT(transaction, result.Data.(*model.Channel)); err != nil { + transaction.Rollback() + result.Err = model.NewAppError("SqlChannelStore.Update", "store.sql_channel.update.upsert_public_channel.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + if err := transaction.Commit(); err != nil { result.Err = model.NewAppError("SqlChannelStore.Update", "store.sql_channel.update.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) return } }) + } func (s SqlChannelStore) updateChannelT(transaction *gorp.Transaction, channel *model.Channel) store.StoreResult { @@ -642,22 +767,16 @@ func (s SqlChannelStore) get(id string, master bool, allowFromCache bool) store. } // Delete records the given deleted timestamp to the channel in question. -// -// @see ChannelStoreExperimental for how this update propagates to the PublicChannels table. func (s SqlChannelStore) Delete(channelId string, time int64) store.StoreChannel { return s.SetDeleteAt(channelId, time, time) } // Restore reverts a previous deleted timestamp from the channel in question. -// -// @see ChannelStoreExperimental for how this update propagates to the PublicChannels table. func (s SqlChannelStore) Restore(channelId string, time int64) store.StoreChannel { return s.SetDeleteAt(channelId, 0, time) } // SetDeleteAt records the given deleted and updated timestamp to the channel in question. -// -// @see ChannelStoreExperimental for how this update propagates to the PublicChannels table. func (s SqlChannelStore) SetDeleteAt(channelId string, deleteAt, updateAt int64) store.StoreChannel { return store.Do(func(result *store.StoreResult) { defer s.InvalidateChannel(channelId) @@ -674,6 +793,23 @@ func (s SqlChannelStore) SetDeleteAt(channelId string, deleteAt, updateAt int64) return } + // 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 { + transaction.Rollback() + result.Err = model.NewAppError("SqlChannelStore.SetDeleteAt", "store.sql_channel.set_delete_at.update_public_channel.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError) + return + } + if err := transaction.Commit(); err != nil { result.Err = model.NewAppError("SqlChannelStore.SetDeleteAt", "store.sql_channel.set_delete_at.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) return @@ -694,8 +830,6 @@ func (s SqlChannelStore) setDeleteAtT(transaction *gorp.Transaction, channelId s } // PermanentDeleteByTeam removes all channels for the given team from the database. -// -// @see ChannelStoreExperimental for how this update propagates to the PublicChannels table. func (s SqlChannelStore) PermanentDeleteByTeam(teamId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { transaction, err := s.GetMaster().Begin() @@ -710,6 +844,20 @@ func (s SqlChannelStore) PermanentDeleteByTeam(teamId string) store.StoreChannel return } + // 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 { + transaction.Rollback() + result.Err = model.NewAppError("SqlChannelStore.PermanentDeleteByTeamt", "store.sql_channel.permanent_delete_by_team.delete_public_channels.app_error", nil, "team_id="+teamId+", "+err.Error(), http.StatusInternalServerError) + return + } + if err := transaction.Commit(); err != nil { result.Err = model.NewAppError("SqlChannelStore.PermanentDeleteByTeam", "store.sql_channel.permanent_delete_by_team.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) return @@ -729,8 +877,6 @@ func (s SqlChannelStore) permanentDeleteByTeamtT(transaction *gorp.Transaction, } // PermanentDelete removes the given channel from the database. -// -// @see ChannelStoreExperimental for how this update propagates to the PublicChannels table. func (s SqlChannelStore) PermanentDelete(channelId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { transaction, err := s.GetMaster().Begin() @@ -745,6 +891,20 @@ func (s SqlChannelStore) PermanentDelete(channelId string) store.StoreChannel { return } + // 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 { + transaction.Rollback() + result.Err = model.NewAppError("SqlChannelStore.PermanentDelete", "store.sql_channel.permanent_delete.delete_public_channel.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError) + return + } + if err := transaction.Commit(); err != nil { result.Err = model.NewAppError("SqlChannelStore.PermanentDelete", "store.sql_channel.permanent_delete.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) return @@ -798,29 +958,38 @@ func (s SqlChannelStore) GetChannels(teamId string, userId string, includeDelete func (s SqlChannelStore) GetMoreChannels(teamId string, userId string, offset int, limit int) store.StoreChannel { return store.Do(func(result *store.StoreResult) { data := &model.ChannelList{} - _, err := s.GetReplica().Select(data, - `SELECT - * + _, err := s.GetReplica().Select(data, ` + SELECT + Channels.* FROM Channels + JOIN + PublicChannels c ON (c.Id = Channels.Id) WHERE - TeamId = :TeamId1 - AND Type IN ('O') - AND DeleteAt = 0 - AND Id NOT IN (SELECT - Channels.Id - FROM - Channels, - ChannelMembers - WHERE - Id = ChannelId - AND TeamId = :TeamId2 - AND UserId = :UserId - AND DeleteAt = 0) - ORDER BY DisplayName + 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{}{"TeamId1": teamId, "TeamId2": teamId, "UserId": userId, "Limit": limit, "Offset": offset}) + OFFSET :Offset + `, map[string]interface{}{ + "TeamId": teamId, + "UserId": userId, + "Limit": limit, + "Offset": offset, + }) if err != nil { result.Err = model.NewAppError("SqlChannelStore.GetMoreChannels", "store.sql_channel.get_more_channels.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error(), http.StatusInternalServerError) @@ -834,19 +1003,24 @@ func (s SqlChannelStore) GetMoreChannels(teamId string, userId string, offset in func (s SqlChannelStore) GetPublicChannelsForTeam(teamId string, offset int, limit int) store.StoreChannel { return store.Do(func(result *store.StoreResult) { data := &model.ChannelList{} - _, err := s.GetReplica().Select(data, - `SELECT - * + _, err := s.GetReplica().Select(data, ` + SELECT + Channels.* FROM Channels + JOIN + PublicChannels pc ON (pc.Id = Channels.Id) WHERE - TeamId = :TeamId - AND Type = 'O' - AND DeleteAt = 0 - ORDER BY DisplayName + pc.TeamId = :TeamId + AND pc.DeleteAt = 0 + ORDER BY pc.DisplayName LIMIT :Limit - OFFSET :Offset`, - map[string]interface{}{"TeamId": teamId, "Limit": limit, "Offset": offset}) + OFFSET :Offset + `, map[string]interface{}{ + "TeamId": teamId, + "Limit": limit, + "Offset": offset, + }) if err != nil { result.Err = model.NewAppError("SqlChannelStore.GetPublicChannelsForTeam", "store.sql_channel.get_public_channels.get.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError) @@ -874,18 +1048,19 @@ func (s SqlChannelStore) GetPublicChannelsByIdsForTeam(teamId string, channelIds } data := &model.ChannelList{} - _, err := s.GetReplica().Select(data, - `SELECT - * + _, err := s.GetReplica().Select(data, ` + SELECT + Channels.* FROM Channels + JOIN + PublicChannels pc ON (pc.Id = Channels.Id) WHERE - TeamId = :teamId - AND Type = 'O' - AND DeleteAt = 0 - AND Id IN (`+idQuery+`) - ORDER BY DisplayName`, - props) + pc.TeamId = :teamId + AND pc.DeleteAt = 0 + AND pc.Id IN (`+idQuery+`) + ORDER BY pc.DisplayName + `, props) if err != nil { result.Err = model.NewAppError("SqlChannelStore.GetPublicChannelsByIdsForTeam", "store.sql_channel.get_channels_by_ids.get.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -1721,33 +1896,35 @@ func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) store.S func (s SqlChannelStore) AutocompleteInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - deleteFilter := "AND DeleteAt = 0" + deleteFilter := "AND c.DeleteAt = 0" if includeDeleted { deleteFilter = "" } queryFormat := ` SELECT - * + Channels.* FROM - Channels + Channels + JOIN + PublicChannels c ON (c.Id = Channels.Id) WHERE - TeamId = :TeamId - AND Type = 'O' - ` + deleteFilter + ` - %v - LIMIT 50` + c.TeamId = :TeamId + ` + deleteFilter + ` + %v + LIMIT 50 + ` var channels model.ChannelList - if likeClause, likeTerm := s.buildLIKEClause(term, "Name, DisplayName, Purpose"); likeClause == "" { + 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 { result.Err = 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, "Name, DisplayName, Purpose") + 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) @@ -1863,53 +2040,61 @@ func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userId string func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - deleteFilter := "AND DeleteAt = 0" + deleteFilter := "AND c.DeleteAt = 0" if includeDeleted { deleteFilter = "" } - searchQuery := ` + + *result = s.performSearch(` SELECT - * + Channels.* FROM - Channels + Channels + JOIN + PublicChannels c ON (c.Id = Channels.Id) WHERE - TeamId = :TeamId - AND Type = 'O' - ` + deleteFilter + ` - SEARCH_CLAUSE - ORDER BY DisplayName - LIMIT 100` - - *result = s.performSearch(searchQuery, term, map[string]interface{}{"TeamId": teamId}) + c.TeamId = :TeamId + `+deleteFilter+` + SEARCH_CLAUSE + ORDER BY c.DisplayName + LIMIT 100 + `, term, map[string]interface{}{ + "TeamId": teamId, + }) }) } func (s SqlChannelStore) SearchMore(userId string, teamId string, term string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - searchQuery := ` + *result = s.performSearch(` SELECT - * + Channels.* FROM Channels + JOIN + PublicChannels c ON (c.Id = Channels.Id) WHERE - TeamId = :TeamId - AND Type = 'O' - AND DeleteAt = 0 - AND Id NOT IN (SELECT - Channels.Id + c.TeamId = :TeamId + AND c.DeleteAt = 0 + AND c.Id NOT IN ( + SELECT + c.Id FROM - Channels, - ChannelMembers + PublicChannels c + JOIN + ChannelMembers cm ON (cm.ChannelId = c.Id) WHERE - Id = ChannelId - AND TeamId = :TeamId - AND UserId = :UserId - AND DeleteAt = 0) - SEARCH_CLAUSE - ORDER BY DisplayName - LIMIT 100` - - *result = s.performSearch(searchQuery, term, map[string]interface{}{"TeamId": teamId, "UserId": userId}) + 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, + }) }) } @@ -1987,13 +2172,13 @@ func (s SqlChannelStore) buildFulltextClause(term string, searchColumns string) func (s SqlChannelStore) performSearch(searchQuery string, term string, parameters map[string]interface{}) store.StoreResult { result := store.StoreResult{} - likeClause, likeTerm := s.buildLIKEClause(term, "Name, DisplayName, Purpose") + 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, "Name, DisplayName, Purpose") + 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) } @@ -2259,19 +2444,6 @@ func (s SqlChannelStore) resetLastPostAtT(transaction *gorp.Transaction) store.S return result } -func (s SqlChannelStore) EnableExperimentalPublicChannelsMaterialization() { - // See SqlChannelStoreExperimental -} - -func (s SqlChannelStore) DisableExperimentalPublicChannelsMaterialization() { - // See SqlChannelStoreExperimental -} - -func (s SqlChannelStore) IsExperimentalPublicChannelsMaterializationEnabled() bool { - // See SqlChannelStoreExperimental - return false -} - func (s SqlChannelStore) GetAllChannelsForExportAfter(limit int, afterId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { var data []*model.ChannelForExport diff --git a/store/sqlstore/channel_store_experimental.go b/store/sqlstore/channel_store_experimental.go deleted file mode 100644 index ed1b4c06a..000000000 --- a/store/sqlstore/channel_store_experimental.go +++ /dev/null @@ -1,778 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sqlstore - -import ( - "fmt" - "net/http" - "sort" - "strconv" - "strings" - "sync/atomic" - - "github.com/pkg/errors" - - "github.com/mattermost/gorp" - "github.com/mattermost/mattermost-server/einterfaces" - "github.com/mattermost/mattermost-server/mlog" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/store" -) - -// 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"` -} - -type SqlChannelStoreExperimental struct { - SqlChannelStore - experimentalPublicChannelsMaterializationDisabled *uint32 -} - -func NewSqlChannelStoreExperimental(sqlStore SqlStore, metrics einterfaces.MetricsInterface, enabled bool) store.ChannelStore { - s := &SqlChannelStoreExperimental{ - SqlChannelStore: *NewSqlChannelStore(sqlStore, metrics).(*SqlChannelStore), - experimentalPublicChannelsMaterializationDisabled: new(uint32), - } - - if enabled { - // Forcibly log, since the default state is enabled and we want this on startup. - mlog.Info("Enabling experimental public channels materialization") - s.EnableExperimentalPublicChannelsMaterialization() - } else { - s.DisableExperimentalPublicChannelsMaterialization() - } - - if s.IsExperimentalPublicChannelsMaterializationEnabled() { - for _, db := range sqlStore.GetAllConns() { - 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) - } - } - - return s -} - -// migratePublicChannels initializes the PublicChannels table with data created before this version -// of the Mattermost server kept it up-to-date. -func (s SqlChannelStoreExperimental) MigratePublicChannels() error { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.MigratePublicChannels() - } - - transaction, err := s.GetMaster().Begin() - if err != nil { - return err - } - - if _, err := transaction.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 - } - - if err := transaction.Commit(); err != nil { - return err - } - - return nil -} - -// DropPublicChannels removes the public channels table. -func (s SqlChannelStoreExperimental) DropPublicChannels() error { - _, err := s.GetMaster().Exec(` - DROP TABLE IF EXISTS PublicChannels - `) - if err != nil { - return errors.Wrap(err, "failed to drop public channels table") - } - - return nil -} - -func (s SqlChannelStoreExperimental) CreateIndexesIfNotExists() { - s.SqlChannelStore.CreateIndexesIfNotExists() - - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return - } - - 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") -} - -func (s SqlChannelStoreExperimental) 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 -} - -func (s SqlChannelStoreExperimental) Save(channel *model.Channel, maxChannelsPerTeam int64) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.Save(channel, maxChannelsPerTeam) - } - - return store.Do(func(result *store.StoreResult) { - if channel.DeleteAt != 0 { - result.Err = model.NewAppError("SqlChannelStoreExperimental.Save", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest) - return - } - - if channel.Type == model.CHANNEL_DIRECT { - result.Err = model.NewAppError("SqlChannelStoreExperimental.Save", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest) - return - } - - transaction, err := s.GetMaster().Begin() - if err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.Save", "store.sql_channel.save.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - - *result = s.saveChannelT(transaction, channel, maxChannelsPerTeam) - if result.Err != nil { - transaction.Rollback() - return - } - - // Additionally propagate the write to the PublicChannels table. - if err := s.upsertPublicChannelT(transaction, result.Data.(*model.Channel)); err != nil { - transaction.Rollback() - result.Err = model.NewAppError("SqlChannelStoreExperimental.Save", "store.sql_channel.save.upsert_public_channel.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - - if err := transaction.Commit(); err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.Save", "store.sql_channel.save.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - }) -} - -func (s SqlChannelStoreExperimental) Update(channel *model.Channel) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.Update(channel) - } - - return store.Do(func(result *store.StoreResult) { - transaction, err := s.GetMaster().Begin() - if err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.Update", "store.sql_channel.update.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - - *result = s.updateChannelT(transaction, channel) - if result.Err != nil { - transaction.Rollback() - return - } - - // Additionally propagate the write to the PublicChannels table. - if err := s.upsertPublicChannelT(transaction, result.Data.(*model.Channel)); err != nil { - transaction.Rollback() - result.Err = model.NewAppError("SqlChannelStoreExperimental.Update", "store.sql_channel.update.upsert_public_channel.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - - if err := transaction.Commit(); err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.Update", "store.sql_channel.update.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - }) -} - -func (s SqlChannelStoreExperimental) Delete(channelId string, time int64) store.StoreChannel { - // Call the experimental version first. - return s.SetDeleteAt(channelId, time, time) -} - -func (s SqlChannelStoreExperimental) Restore(channelId string, time int64) store.StoreChannel { - // Call the experimental version first. - return s.SetDeleteAt(channelId, 0, time) -} - -func (s SqlChannelStoreExperimental) SetDeleteAt(channelId string, deleteAt, updateAt int64) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.SetDeleteAt(channelId, deleteAt, updateAt) - } - - return store.Do(func(result *store.StoreResult) { - defer s.InvalidateChannel(channelId) - - transaction, err := s.GetMaster().Begin() - if err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.SetDeleteAt", "store.sql_channel.set_delete_at.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - - *result = s.setDeleteAtT(transaction, channelId, deleteAt, updateAt) - if result.Err != nil { - transaction.Rollback() - return - } - - // 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 { - transaction.Rollback() - result.Err = model.NewAppError("SqlChannelStoreExperimental.SetDeleteAt", "store.sql_channel.set_delete_at.update_public_channel.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError) - return - } - - if err := transaction.Commit(); err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.SetDeleteAt", "store.sql_channel.set_delete_at.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - }) -} - -func (s SqlChannelStoreExperimental) PermanentDeleteByTeam(teamId string) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.PermanentDeleteByTeam(teamId) - } - - return store.Do(func(result *store.StoreResult) { - transaction, err := s.GetMaster().Begin() - if err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.PermanentDeleteByTeam", "store.sql_channel.permanent_delete_by_team.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - - *result = s.permanentDeleteByTeamtT(transaction, teamId) - if result.Err != nil { - transaction.Rollback() - return - } - - // 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 { - transaction.Rollback() - result.Err = model.NewAppError("SqlChannelStoreExperimental.PermanentDeleteByTeamt", "store.sql_channel.permanent_delete_by_team.delete_public_channels.app_error", nil, "team_id="+teamId+", "+err.Error(), http.StatusInternalServerError) - return - } - - if err := transaction.Commit(); err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.PermanentDeleteByTeam", "store.sql_channel.permanent_delete_by_team.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - }) -} - -func (s SqlChannelStoreExperimental) PermanentDelete(channelId string) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.PermanentDelete(channelId) - } - - return store.Do(func(result *store.StoreResult) { - transaction, err := s.GetMaster().Begin() - if err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.PermanentDelete", "store.sql_channel.permanent_delete.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - - *result = s.permanentDeleteT(transaction, channelId) - if result.Err != nil { - transaction.Rollback() - return - } - - // 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 { - transaction.Rollback() - result.Err = model.NewAppError("SqlChannelStoreExperimental.PermanentDelete", "store.sql_channel.permanent_delete.delete_public_channel.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError) - return - } - - if err := transaction.Commit(); err != nil { - result.Err = model.NewAppError("SqlChannelStoreExperimental.PermanentDelete", "store.sql_channel.permanent_delete.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) - return - } - }) -} - -func (s SqlChannelStoreExperimental) GetMoreChannels(teamId string, userId string, offset int, limit int) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.GetMoreChannels(teamId, userId, offset, limit) - } - - return store.Do(func(result *store.StoreResult) { - data := &model.ChannelList{} - _, err := s.GetReplica().Select(data, ` - 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 { - result.Err = model.NewAppError("SqlChannelStore.GetMoreChannels", "store.sql_channel.get_more_channels.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error(), http.StatusInternalServerError) - return - } - - result.Data = data - }) -} - -func (s SqlChannelStoreExperimental) GetPublicChannelsForTeam(teamId string, offset int, limit int) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.GetPublicChannelsForTeam(teamId, offset, limit) - } - - return store.Do(func(result *store.StoreResult) { - 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 - ORDER BY pc.DisplayName - LIMIT :Limit - OFFSET :Offset - `, map[string]interface{}{ - "TeamId": teamId, - "Limit": limit, - "Offset": offset, - }) - - if err != nil { - result.Err = model.NewAppError("SqlChannelStore.GetPublicChannelsForTeam", "store.sql_channel.get_public_channels.get.app_error", nil, "teamId="+teamId+", err="+err.Error(), http.StatusInternalServerError) - return - } - - result.Data = data - }) -} - -func (s SqlChannelStoreExperimental) GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.GetPublicChannelsByIdsForTeam(teamId, channelIds) - } - - return store.Do(func(result *store.StoreResult) { - 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 { - result.Err = model.NewAppError("SqlChannelStore.GetPublicChannelsByIdsForTeam", "store.sql_channel.get_channels_by_ids.get.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - if len(*data) == 0 { - result.Err = model.NewAppError("SqlChannelStore.GetPublicChannelsByIdsForTeam", "store.sql_channel.get_channels_by_ids.not_found.app_error", nil, "", http.StatusNotFound) - } - - result.Data = data - }) -} - -func (s SqlChannelStoreExperimental) AutocompleteInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.AutocompleteInTeam(teamId, term, includeDeleted) - } - - return store.Do(func(result *store.StoreResult) { - deleteFilter := "AND c.DeleteAt = 0" - if includeDeleted { - deleteFilter = "" - } - - queryFormat := ` - SELECT - Channels.* - FROM - Channels - JOIN - PublicChannels c ON (c.Id = Channels.Id) - WHERE - c.TeamId = :TeamId - ` + deleteFilter + ` - %v - LIMIT 50 - ` - - 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 { - result.Err = 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 { - result.Err = 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) - }) - result.Data = &channels - }) -} - -func (s SqlChannelStoreExperimental) SearchInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.SearchInTeam(teamId, term, includeDeleted) - } - - return store.Do(func(result *store.StoreResult) { - deleteFilter := "AND c.DeleteAt = 0" - if includeDeleted { - deleteFilter = "" - } - - *result = 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 SqlChannelStoreExperimental) SearchMore(userId string, teamId string, term string) store.StoreChannel { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.SearchMore(userId, teamId, term) - } - - return store.Do(func(result *store.StoreResult) { - *result = 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 SqlChannelStoreExperimental) buildLIKEClause(term string, searchColumns string) (likeClause, likeTerm string) { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.buildLIKEClause(term, searchColumns) - } - - likeTerm = term - - // These chars must be removed from the like query. - for _, c := range ignoreLikeSearchChar { - likeTerm = strings.Replace(likeTerm, c, "", -1) - } - - // These chars must be escaped in the like query. - for _, c := range escapeLikeSearchChar { - likeTerm = strings.Replace(likeTerm, c, "*"+c, -1) - } - - 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 += "%" - return -} - -func (s SqlChannelStoreExperimental) buildFulltextClause(term string, searchColumns string) (fulltextClause, fulltextTerm string) { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.buildFulltextClause(term, searchColumns) - } - - // 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("((%s) @@ to_tsquery(: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 SqlChannelStoreExperimental) performSearch(searchQuery string, term string, parameters map[string]interface{}) store.StoreResult { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - return s.SqlChannelStore.performSearch(searchQuery, term, parameters) - } - - result := store.StoreResult{} - - 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 { - result.Err = model.NewAppError("SqlChannelStore.Search", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError) - return result - } - - result.Data = &channels - return result -} - -func (s SqlChannelStoreExperimental) EnableExperimentalPublicChannelsMaterialization() { - if !s.IsExperimentalPublicChannelsMaterializationEnabled() { - mlog.Info("Enabling experimental public channels materialization") - } - - atomic.StoreUint32(s.experimentalPublicChannelsMaterializationDisabled, 0) -} - -func (s SqlChannelStoreExperimental) DisableExperimentalPublicChannelsMaterialization() { - if s.IsExperimentalPublicChannelsMaterializationEnabled() { - mlog.Info("Disabling experimental public channels materialization") - } - - atomic.StoreUint32(s.experimentalPublicChannelsMaterializationDisabled, 1) -} - -func (s SqlChannelStoreExperimental) IsExperimentalPublicChannelsMaterializationEnabled() bool { - return atomic.LoadUint32(s.experimentalPublicChannelsMaterializationDisabled) == 0 -} diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index 2fc299dc9..c0c92aaaf 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -118,13 +118,8 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter supplier.initConnection() - enableExperimentalPublicChannelsMaterialization := true - if settings.EnablePublicChannelsMaterialization != nil && !*settings.EnablePublicChannelsMaterialization { - enableExperimentalPublicChannelsMaterialization = false - } - supplier.oldStores.team = NewSqlTeamStore(supplier) - supplier.oldStores.channel = NewSqlChannelStoreExperimental(supplier, metrics, enableExperimentalPublicChannelsMaterialization) + supplier.oldStores.channel = NewSqlChannelStore(supplier, metrics) supplier.oldStores.post = NewSqlPostStore(supplier, metrics) supplier.oldStores.user = NewSqlUserStore(supplier, metrics) supplier.oldStores.audit = NewSqlAuditStore(supplier) @@ -162,7 +157,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter UpgradeDatabase(supplier) supplier.oldStores.team.(*SqlTeamStore).CreateIndexesIfNotExists() - supplier.oldStores.channel.(*SqlChannelStoreExperimental).CreateIndexesIfNotExists() + supplier.oldStores.channel.(*SqlChannelStore).CreateIndexesIfNotExists() supplier.oldStores.post.(*SqlPostStore).CreateIndexesIfNotExists() supplier.oldStores.user.(*SqlUserStore).CreateIndexesIfNotExists() supplier.oldStores.audit.(*SqlAuditStore).CreateIndexesIfNotExists() diff --git a/store/store.go b/store/store.go index e554f1486..eefaa4649 100644 --- a/store/store.go +++ b/store/store.go @@ -179,10 +179,6 @@ type ChannelStore interface { ClearAllCustomRoleAssignments() StoreChannel ResetLastPostAt() StoreChannel MigratePublicChannels() error - DropPublicChannels() error - EnableExperimentalPublicChannelsMaterialization() - DisableExperimentalPublicChannelsMaterialization() - IsExperimentalPublicChannelsMaterializationEnabled() bool GetAllChannelsForExportAfter(limit int, afterId string) StoreChannel GetChannelMembersForExport(userId string, teamId string) StoreChannel RemoveAllDeactivatedMembers(channelId string) StoreChannel diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 34efd810d..7fc419061 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -24,65 +24,48 @@ type SqlSupplier interface { func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { createDefaultRoles(t, ss) - for _, enabled := range []bool{true, false} { - description := "experimental materialization" - if enabled { - description += " enabled" - ss.Channel().EnableExperimentalPublicChannelsMaterialization() - } else { - description += " disabled" - ss.Channel().DisableExperimentalPublicChannelsMaterialization() - - // Additionally drop the public channels table and all associated triggers - // to prove that the experimental store is fully disabled. - ss.Channel().DropPublicChannels() - } - - t.Run(description, func(t *testing.T) { - t.Run("Save", func(t *testing.T) { testChannelStoreSave(t, ss) }) - t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, ss) }) - t.Run("CreateDirectChannel", func(t *testing.T) { testChannelStoreCreateDirectChannel(t, ss) }) - t.Run("Update", func(t *testing.T) { testChannelStoreUpdate(t, ss) }) - t.Run("GetChannelUnread", func(t *testing.T) { testGetChannelUnread(t, ss) }) - t.Run("Get", func(t *testing.T) { testChannelStoreGet(t, ss) }) - t.Run("GetForPost", func(t *testing.T) { testChannelStoreGetForPost(t, ss) }) - t.Run("Restore", func(t *testing.T) { testChannelStoreRestore(t, ss) }) - t.Run("Delete", func(t *testing.T) { testChannelStoreDelete(t, ss) }) - t.Run("GetByName", func(t *testing.T) { testChannelStoreGetByName(t, ss) }) - t.Run("GetByNames", func(t *testing.T) { testChannelStoreGetByNames(t, ss) }) - t.Run("GetDeletedByName", func(t *testing.T) { testChannelStoreGetDeletedByName(t, ss) }) - t.Run("GetDeleted", func(t *testing.T) { testChannelStoreGetDeleted(t, ss) }) - t.Run("ChannelMemberStore", func(t *testing.T) { testChannelMemberStore(t, ss) }) - t.Run("ChannelDeleteMemberStore", func(t *testing.T) { testChannelDeleteMemberStore(t, ss) }) - t.Run("GetChannels", func(t *testing.T) { testChannelStoreGetChannels(t, ss) }) - t.Run("GetMoreChannels", func(t *testing.T) { testChannelStoreGetMoreChannels(t, ss) }) - t.Run("GetPublicChannelsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsForTeam(t, ss) }) - t.Run("GetPublicChannelsByIdsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsByIdsForTeam(t, ss) }) - t.Run("GetChannelCounts", func(t *testing.T) { testChannelStoreGetChannelCounts(t, ss) }) - t.Run("GetMembersForUser", func(t *testing.T) { testChannelStoreGetMembersForUser(t, ss) }) - t.Run("UpdateLastViewedAt", func(t *testing.T) { testChannelStoreUpdateLastViewedAt(t, ss) }) - t.Run("IncrementMentionCount", func(t *testing.T) { testChannelStoreIncrementMentionCount(t, ss) }) - t.Run("UpdateChannelMember", func(t *testing.T) { testUpdateChannelMember(t, ss) }) - t.Run("GetMember", func(t *testing.T) { testGetMember(t, ss) }) - t.Run("GetMemberForPost", func(t *testing.T) { testChannelStoreGetMemberForPost(t, ss) }) - t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) }) - t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) }) - t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) - t.Run("AutocompleteInTeamForSearch", func(t *testing.T) { testChannelStoreAutocompleteInTeamForSearch(t, ss) }) - t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) }) - t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) - t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) - t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) }) - t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) }) - t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) }) - t.Run("ResetAllChannelSchemes", func(t *testing.T) { testResetAllChannelSchemes(t, ss) }) - t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testChannelStoreClearAllCustomRoleAssignments(t, ss) }) - t.Run("MaterializedPublicChannels", func(t *testing.T) { testMaterializedPublicChannels(t, ss, s) }) - t.Run("GetAllChannelsForExportAfter", func(t *testing.T) { testChannelStoreGetAllChannelsForExportAfter(t, ss) }) - t.Run("GetChannelMembersForExport", func(t *testing.T) { testChannelStoreGetChannelMembersForExport(t, ss) }) - t.Run("RemoveAllDeactivatedMembers", func(t *testing.T) { testChannelStoreRemoveAllDeactivatedMembers(t, ss) }) - }) - } + t.Run("Save", func(t *testing.T) { testChannelStoreSave(t, ss) }) + t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, ss) }) + t.Run("CreateDirectChannel", func(t *testing.T) { testChannelStoreCreateDirectChannel(t, ss) }) + t.Run("Update", func(t *testing.T) { testChannelStoreUpdate(t, ss) }) + t.Run("GetChannelUnread", func(t *testing.T) { testGetChannelUnread(t, ss) }) + t.Run("Get", func(t *testing.T) { testChannelStoreGet(t, ss) }) + t.Run("GetForPost", func(t *testing.T) { testChannelStoreGetForPost(t, ss) }) + t.Run("Restore", func(t *testing.T) { testChannelStoreRestore(t, ss) }) + t.Run("Delete", func(t *testing.T) { testChannelStoreDelete(t, ss) }) + t.Run("GetByName", func(t *testing.T) { testChannelStoreGetByName(t, ss) }) + t.Run("GetByNames", func(t *testing.T) { testChannelStoreGetByNames(t, ss) }) + t.Run("GetDeletedByName", func(t *testing.T) { testChannelStoreGetDeletedByName(t, ss) }) + t.Run("GetDeleted", func(t *testing.T) { testChannelStoreGetDeleted(t, ss) }) + t.Run("ChannelMemberStore", func(t *testing.T) { testChannelMemberStore(t, ss) }) + t.Run("ChannelDeleteMemberStore", func(t *testing.T) { testChannelDeleteMemberStore(t, ss) }) + t.Run("GetChannels", func(t *testing.T) { testChannelStoreGetChannels(t, ss) }) + t.Run("GetMoreChannels", func(t *testing.T) { testChannelStoreGetMoreChannels(t, ss) }) + t.Run("GetPublicChannelsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsForTeam(t, ss) }) + t.Run("GetPublicChannelsByIdsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsByIdsForTeam(t, ss) }) + t.Run("GetChannelCounts", func(t *testing.T) { testChannelStoreGetChannelCounts(t, ss) }) + t.Run("GetMembersForUser", func(t *testing.T) { testChannelStoreGetMembersForUser(t, ss) }) + t.Run("UpdateLastViewedAt", func(t *testing.T) { testChannelStoreUpdateLastViewedAt(t, ss) }) + t.Run("IncrementMentionCount", func(t *testing.T) { testChannelStoreIncrementMentionCount(t, ss) }) + t.Run("UpdateChannelMember", func(t *testing.T) { testUpdateChannelMember(t, ss) }) + t.Run("GetMember", func(t *testing.T) { testGetMember(t, ss) }) + t.Run("GetMemberForPost", func(t *testing.T) { testChannelStoreGetMemberForPost(t, ss) }) + t.Run("GetMemberCount", func(t *testing.T) { testGetMemberCount(t, ss) }) + t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) }) + t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) + t.Run("AutocompleteInTeamForSearch", func(t *testing.T) { testChannelStoreAutocompleteInTeamForSearch(t, ss) }) + t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) }) + t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) + t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) + t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) }) + t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) }) + t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) }) + t.Run("ResetAllChannelSchemes", func(t *testing.T) { testResetAllChannelSchemes(t, ss) }) + t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testChannelStoreClearAllCustomRoleAssignments(t, ss) }) + t.Run("MaterializedPublicChannels", func(t *testing.T) { testMaterializedPublicChannels(t, ss, s) }) + t.Run("GetAllChannelsForExportAfter", func(t *testing.T) { testChannelStoreGetAllChannelsForExportAfter(t, ss) }) + t.Run("GetChannelMembersForExport", func(t *testing.T) { testChannelStoreGetChannelMembersForExport(t, ss) }) + t.Run("RemoveAllDeactivatedMembers", func(t *testing.T) { testChannelStoreRemoveAllDeactivatedMembers(t, ss) }) } func testChannelStoreSave(t *testing.T, ss store.Store) { @@ -2574,10 +2557,6 @@ func testChannelStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) // testMaterializedPublicChannels tests edge cases involving the triggers and stored procedures // that materialize the PublicChannels table. func testMaterializedPublicChannels(t *testing.T, ss store.Store, s SqlSupplier) { - if !ss.Channel().IsExperimentalPublicChannelsMaterializationEnabled() { - return - } - teamId := model.NewId() // o1 is a public channel on the team diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 7ad8f100d..b12d2a000 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -130,30 +130,6 @@ func (_m *ChannelStore) Delete(channelId string, time int64) store.StoreChannel return r0 } -// DisableExperimentalPublicChannelsMaterialization provides a mock function with given fields: -func (_m *ChannelStore) DisableExperimentalPublicChannelsMaterialization() { - _m.Called() -} - -// DropPublicChannels provides a mock function with given fields: -func (_m *ChannelStore) DropPublicChannels() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// EnableExperimentalPublicChannelsMaterialization provides a mock function with given fields: -func (_m *ChannelStore) EnableExperimentalPublicChannelsMaterialization() { - _m.Called() -} - // Get provides a mock function with given fields: id, allowFromCache func (_m *ChannelStore) Get(id string, allowFromCache bool) store.StoreChannel { ret := _m.Called(id, allowFromCache) @@ -673,20 +649,6 @@ func (_m *ChannelStore) InvalidateMemberCount(channelId string) { _m.Called(channelId) } -// IsExperimentalPublicChannelsMaterializationEnabled provides a mock function with given fields: -func (_m *ChannelStore) IsExperimentalPublicChannelsMaterializationEnabled() bool { - ret := _m.Called() - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - // IsUserInChannelUseCache provides a mock function with given fields: userId, channelId func (_m *ChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool { ret := _m.Called(userId, channelId) -- cgit v1.2.3-1-g7c22