Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -0,0 +1,905 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
//nolint:gosec
"crypto/md5"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func boardFields(tableAlias string) []string {
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
tableAlias += "."
}
return []string{
tableAlias + "id",
tableAlias + "team_id",
"COALESCE(" + tableAlias + "channel_id, '')",
"COALESCE(" + tableAlias + "created_by, '')",
tableAlias + "modified_by",
tableAlias + "type",
tableAlias + "minimum_role",
tableAlias + "title",
tableAlias + "description",
tableAlias + "icon",
tableAlias + "show_description",
tableAlias + "is_template",
tableAlias + "template_version",
"COALESCE(" + tableAlias + "properties, '{}')",
"COALESCE(" + tableAlias + "card_properties, '[]')",
tableAlias + "create_at",
tableAlias + "update_at",
tableAlias + "delete_at",
}
}
func boardHistoryFields() []string {
fields := []string{
"id",
"team_id",
"COALESCE(channel_id, '')",
"COALESCE(created_by, '')",
"COALESCE(modified_by, '')",
"type",
"minimum_role",
"COALESCE(title, '')",
"COALESCE(description, '')",
"COALESCE(icon, '')",
"COALESCE(show_description, false)",
"COALESCE(is_template, false)",
"template_version",
"COALESCE(properties, '{}')",
"COALESCE(card_properties, '[]')",
"COALESCE(create_at, 0)",
"COALESCE(update_at, 0)",
"COALESCE(delete_at, 0)",
}
return fields
}
var boardMemberFields = []string{
"COALESCE(B.minimum_role, '')",
"BM.board_id",
"BM.user_id",
"BM.roles",
"BM.scheme_admin",
"BM.scheme_editor",
"BM.scheme_commenter",
"BM.scheme_viewer",
}
func (s *SQLStore) boardsFromRows(rows *sql.Rows) ([]*model.Board, error) {
boards := []*model.Board{}
for rows.Next() {
var board model.Board
var propertiesBytes []byte
var cardPropertiesBytes []byte
err := rows.Scan(
&board.ID,
&board.TeamID,
&board.ChannelID,
&board.CreatedBy,
&board.ModifiedBy,
&board.Type,
&board.MinimumRole,
&board.Title,
&board.Description,
&board.Icon,
&board.ShowDescription,
&board.IsTemplate,
&board.TemplateVersion,
&propertiesBytes,
&cardPropertiesBytes,
&board.CreateAt,
&board.UpdateAt,
&board.DeleteAt,
)
if err != nil {
s.logger.Error("boardsFromRows scan error", mlog.Err(err))
return nil, err
}
err = json.Unmarshal(propertiesBytes, &board.Properties)
if err != nil {
s.logger.Error("board properties unmarshal error", mlog.Err(err))
return nil, err
}
err = json.Unmarshal(cardPropertiesBytes, &board.CardProperties)
if err != nil {
s.logger.Error("board card properties unmarshal error", mlog.Err(err))
return nil, err
}
boards = append(boards, &board)
}
return boards, nil
}
func (s *SQLStore) boardMembersFromRows(rows *sql.Rows) ([]*model.BoardMember, error) {
boardMembers := []*model.BoardMember{}
for rows.Next() {
var boardMember model.BoardMember
err := rows.Scan(
&boardMember.MinimumRole,
&boardMember.BoardID,
&boardMember.UserID,
&boardMember.Roles,
&boardMember.SchemeAdmin,
&boardMember.SchemeEditor,
&boardMember.SchemeCommenter,
&boardMember.SchemeViewer,
)
if err != nil {
return nil, err
}
boardMembers = append(boardMembers, &boardMember)
}
return boardMembers, nil
}
func (s *SQLStore) boardMemberHistoryEntriesFromRows(rows *sql.Rows) ([]*model.BoardMemberHistoryEntry, error) {
boardMemberHistoryEntries := []*model.BoardMemberHistoryEntry{}
for rows.Next() {
var boardMemberHistoryEntry model.BoardMemberHistoryEntry
var insertAt sql.NullString
err := rows.Scan(
&boardMemberHistoryEntry.BoardID,
&boardMemberHistoryEntry.UserID,
&boardMemberHistoryEntry.Action,
&insertAt,
)
if err != nil {
return nil, err
}
// parse the insert_at timestamp which is different based on database type.
dateTemplate := "2006-01-02T15:04:05Z0700"
if s.dbType == model.MysqlDBType {
dateTemplate = "2006-01-02 15:04:05.000000"
}
ts, err := time.Parse(dateTemplate, insertAt.String)
if err != nil {
return nil, fmt.Errorf("cannot parse datetime '%s' for board_members_history scan: %w", insertAt.String, err)
}
boardMemberHistoryEntry.InsertAt = ts
boardMemberHistoryEntries = append(boardMemberHistoryEntries, &boardMemberHistoryEntry)
}
return boardMemberHistoryEntries, nil
}
func (s *SQLStore) getBoardByCondition(db sq.BaseRunner, conditions ...interface{}) (*model.Board, error) {
boards, err := s.getBoardsByCondition(db, conditions...)
if err != nil {
return nil, err
}
return boards[0], nil
}
func (s *SQLStore) getBoardsByCondition(db sq.BaseRunner, conditions ...interface{}) ([]*model.Board, error) {
return s.getBoardsFieldsByCondition(db, boardFields(""), conditions...)
}
func (s *SQLStore) getBoardsFieldsByCondition(db sq.BaseRunner, fields []string, conditions ...interface{}) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(fields...).
From(s.tablePrefix + "boards")
for _, c := range conditions {
query = query.Where(c)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardsFieldsByCondition ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, err
}
if len(boards) == 0 {
return nil, model.NewErrNotFound("boards")
}
return boards, nil
}
func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, error) {
return s.getBoardByCondition(db, sq.Eq{"id": boardID})
}
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
Distinct().
From(s.tablePrefix + "boards as b").
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
Where(sq.Eq{"b.team_id": teamID}).
Where(sq.Eq{"b.is_template": false})
if includePublicBoards {
query = query.Where(sq.Or{
sq.Eq{"b.type": model.BoardTypeOpen},
sq.Eq{"bm.user_id": userID},
})
} else {
query = query.Where(sq.Or{
sq.Eq{"bm.user_id": userID},
})
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardsForUserAndTeam ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows)
}
func (s *SQLStore) getBoardsInTeamByIds(db sq.BaseRunner, boardIDs []string, teamID string) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b").
Where(sq.Eq{"b.team_id": teamID}).
Where(sq.Eq{"b.id": boardIDs})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardsInTeamByIds ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, err
}
if len(boards) != len(boardIDs) {
s.logger.Warn("getBoardsInTeamByIds mismatched number of boards found",
mlog.Int("len(boards)", len(boards)),
mlog.Int("len(boardIDs)", len(boardIDs)),
)
return boards, model.NewErrNotAllFound("board", boardIDs)
}
return boards, nil
}
func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, error) {
// Generate tracking IDs for in-built templates
if board.IsTemplate && board.TeamID == model.GlobalTeamID {
//nolint:gosec
// we don't need cryptographically secure hash, so MD5 is fine
board.Properties["trackingTemplateId"] = fmt.Sprintf("%x", md5.Sum([]byte(board.Title)))
}
propertiesBytes, err := s.MarshalJSONB(board.Properties)
if err != nil {
s.logger.Error(
"failed to marshal board.Properties",
mlog.String("board_id", board.ID),
mlog.String("board.Properties", fmt.Sprintf("%v", board.Properties)),
mlog.Err(err),
)
return nil, err
}
cardPropertiesBytes, err := s.MarshalJSONB(board.CardProperties)
if err != nil {
s.logger.Error(
"failed to marshal board.CardProperties",
mlog.String("board_id", board.ID),
mlog.String("board.CardProperties", fmt.Sprintf("%v", board.CardProperties)),
mlog.Err(err),
)
return nil, err
}
existingBoard, err := s.getBoard(db, board.ID)
if err != nil && !model.IsErrNotFound(err) {
return nil, fmt.Errorf("insertBoard error occurred while fetching existing board %s: %w", board.ID, err)
}
insertQuery := s.getQueryBuilder(db).Insert("").
Columns(boardFields("")...)
now := utils.GetMillis()
board.ModifiedBy = userID
board.UpdateAt = now
insertQueryValues := map[string]interface{}{
"id": board.ID,
"team_id": board.TeamID,
"channel_id": board.ChannelID,
"created_by": board.CreatedBy,
"modified_by": board.ModifiedBy,
"type": board.Type,
"title": board.Title,
"minimum_role": board.MinimumRole,
"description": board.Description,
"icon": board.Icon,
"show_description": board.ShowDescription,
"is_template": board.IsTemplate,
"template_version": board.TemplateVersion,
"properties": propertiesBytes,
"card_properties": cardPropertiesBytes,
"create_at": board.CreateAt,
"update_at": board.UpdateAt,
"delete_at": board.DeleteAt,
}
if existingBoard != nil {
query := s.getQueryBuilder(db).Update(s.tablePrefix+"boards").
Where(sq.Eq{"id": board.ID}).
Set("modified_by", board.ModifiedBy).
Set("type", board.Type).
Set("channel_id", board.ChannelID).
Set("minimum_role", board.MinimumRole).
Set("title", board.Title).
Set("description", board.Description).
Set("icon", board.Icon).
Set("show_description", board.ShowDescription).
Set("is_template", board.IsTemplate).
Set("template_version", board.TemplateVersion).
Set("properties", propertiesBytes).
Set("card_properties", cardPropertiesBytes).
Set("update_at", board.UpdateAt).
Set("delete_at", board.DeleteAt)
if _, err := query.Exec(); err != nil {
s.logger.Error(`InsertBoard error occurred while updating existing board`, mlog.String("boardID", board.ID), mlog.Err(err))
return nil, fmt.Errorf("insertBoard error occurred while updating existing board %s: %w", board.ID, err)
}
} else {
board.CreatedBy = userID
board.CreateAt = now
insertQueryValues["created_by"] = board.CreatedBy
insertQueryValues["create_at"] = board.CreateAt
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards")
if _, err := query.Exec(); err != nil {
return nil, fmt.Errorf("insertBoard error occurred while inserting board %s: %w", board.ID, err)
}
}
// writing board history
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history")
if _, err := query.Exec(); err != nil {
s.logger.Error("failed to insert board history", mlog.String("board_id", board.ID), mlog.Err(err))
return nil, fmt.Errorf("failed to insert board %s history: %w", board.ID, err)
}
return board, nil
}
func (s *SQLStore) patchBoard(db sq.BaseRunner, boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error) {
existingBoard, err := s.getBoard(db, boardID)
if err != nil {
return nil, err
}
board := boardPatch.Patch(existingBoard)
return s.insertBoard(db, board, userID)
}
func (s *SQLStore) deleteBoard(db sq.BaseRunner, boardID, userID string) error {
return s.deleteBoardAndChildren(db, boardID, userID, false)
}
func (s *SQLStore) deleteBoardAndChildren(db sq.BaseRunner, boardID, userID string, keepChildren bool) error {
now := utils.GetMillis()
board, err := s.getBoard(db, boardID)
if err != nil {
return err
}
propertiesBytes, err := s.MarshalJSONB(board.Properties)
if err != nil {
return err
}
cardPropertiesBytes, err := s.MarshalJSONB(board.CardProperties)
if err != nil {
return err
}
insertQueryValues := map[string]interface{}{
"id": board.ID,
"team_id": board.TeamID,
"channel_id": board.ChannelID,
"created_by": board.CreatedBy,
"modified_by": userID,
"type": board.Type,
"minimum_role": board.MinimumRole,
"title": board.Title,
"description": board.Description,
"icon": board.Icon,
"show_description": board.ShowDescription,
"is_template": board.IsTemplate,
"template_version": board.TemplateVersion,
"properties": propertiesBytes,
"card_properties": cardPropertiesBytes,
"create_at": board.CreateAt,
"update_at": now,
"delete_at": now,
}
// writing board history
insertQuery := s.getQueryBuilder(db).Insert("").
Columns(boardHistoryFields()...)
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history")
if _, err := query.Exec(); err != nil {
return err
}
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + "boards").
Where(sq.Eq{"id": boardID}).
Where(sq.Eq{"COALESCE(team_id, '0')": board.TeamID})
if _, err := deleteQuery.Exec(); err != nil {
return err
}
if keepChildren {
return nil
}
return s.deleteBlockChildren(db, boardID, "", userID)
}
func (s *SQLStore) insertBoardWithAdmin(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, *model.BoardMember, error) {
newBoard, err := s.insertBoard(db, board, userID)
if err != nil {
return nil, nil, err
}
bm := &model.BoardMember{
BoardID: newBoard.ID,
UserID: newBoard.CreatedBy,
SchemeAdmin: true,
SchemeEditor: true,
}
nbm, err := s.saveMember(db, bm)
if err != nil {
return nil, nil, fmt.Errorf("cannot save member %s while inserting board %s: %w", bm.UserID, bm.BoardID, err)
}
return newBoard, nbm, nil
}
func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.BoardMember, error) {
queryValues := map[string]interface{}{
"board_id": bm.BoardID,
"user_id": bm.UserID,
"roles": "",
"scheme_admin": bm.SchemeAdmin,
"scheme_editor": bm.SchemeEditor,
"scheme_commenter": bm.SchemeCommenter,
"scheme_viewer": bm.SchemeViewer,
}
oldMember, err := s.getMemberForBoard(db, bm.BoardID, bm.UserID)
if err != nil && !model.IsErrNotFound(err) {
return nil, err
}
query := s.getQueryBuilder(db).
Insert(s.tablePrefix + "board_members").
SetMap(queryValues)
if s.dbType == model.MysqlDBType {
query = query.Suffix(
"ON DUPLICATE KEY UPDATE scheme_admin = ?, scheme_editor = ?, scheme_commenter = ?, scheme_viewer = ?",
bm.SchemeAdmin, bm.SchemeEditor, bm.SchemeCommenter, bm.SchemeViewer)
} else {
query = query.Suffix(
`ON CONFLICT (board_id, user_id)
DO UPDATE SET scheme_admin = EXCLUDED.scheme_admin, scheme_editor = EXCLUDED.scheme_editor,
scheme_commenter = EXCLUDED.scheme_commenter, scheme_viewer = EXCLUDED.scheme_viewer`,
)
}
if _, err := query.Exec(); err != nil {
return nil, err
}
if oldMember == nil {
addToMembersHistory := s.getQueryBuilder(db).
Insert(s.tablePrefix+"board_members_history").
Columns("board_id", "user_id", "action").
Values(bm.BoardID, bm.UserID, "created")
if _, err := addToMembersHistory.Exec(); err != nil {
return nil, err
}
}
return bm, nil
}
func (s *SQLStore) deleteMember(db sq.BaseRunner, boardID, userID string) error {
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + "board_members").
Where(sq.Eq{"board_id": boardID}).
Where(sq.Eq{"user_id": userID})
result, err := deleteQuery.Exec()
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected > 0 {
addToMembersHistory := s.getQueryBuilder(db).
Insert(s.tablePrefix+"board_members_history").
Columns("board_id", "user_id", "action").
Values(boardID, userID, "deleted")
if _, err := addToMembersHistory.Exec(); err != nil {
return err
}
}
return nil
}
func (s *SQLStore) getMemberForBoard(db sq.BaseRunner, boardID, userID string) (*model.BoardMember, error) {
query := s.getQueryBuilder(db).
Select(boardMemberFields...).
From(s.tablePrefix + "board_members AS BM").
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
Where(sq.Eq{"BM.board_id": boardID}).
Where(sq.Eq{"BM.user_id": userID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getMemberForBoard ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
members, err := s.boardMembersFromRows(rows)
if err != nil {
return nil, err
}
if len(members) == 0 {
message := fmt.Sprintf("board member BoardID=%s UserID=%s", boardID, userID)
return nil, model.NewErrNotFound(message)
}
return members[0], nil
}
func (s *SQLStore) getMembersForUser(db sq.BaseRunner, userID string) ([]*model.BoardMember, error) {
query := s.getQueryBuilder(db).
Select(boardMemberFields...).
From(s.tablePrefix + "board_members AS BM").
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
Where(sq.Eq{"BM.user_id": userID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getMembersForUser ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
members, err := s.boardMembersFromRows(rows)
if err != nil {
return nil, err
}
return members, nil
}
func (s *SQLStore) getMembersForBoard(db sq.BaseRunner, boardID string) ([]*model.BoardMember, error) {
query := s.getQueryBuilder(db).
Select(boardMemberFields...).
From(s.tablePrefix + "board_members AS BM").
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
Where(sq.Eq{"BM.board_id": boardID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`getMembersForBoard ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardMembersFromRows(rows)
}
// searchBoardsForUser returns all boards that match with the
// term that are either private and which the user is a member of, or
// they're open, regardless of the user membership.
// Search is case-insensitive.
func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term string, searchField model.BoardSearchField, userID string, includePublicBoards bool) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
Distinct().
From(s.tablePrefix + "boards as b").
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
Where(sq.Eq{"b.is_template": false})
if includePublicBoards {
query = query.Where(sq.Or{
sq.Eq{"b.type": model.BoardTypeOpen},
sq.Eq{"bm.user_id": userID},
})
} else {
query = query.Where(sq.Or{
sq.Eq{"bm.user_id": userID},
})
}
if term != "" {
if searchField == model.BoardSearchFieldPropertyName {
switch s.dbType {
case model.PostgresDBType:
where := "b.properties->? is not null"
query = query.Where(where, term)
case model.MysqlDBType:
where := "JSON_EXTRACT(b.properties, ?) IS NOT NULL"
query = query.Where(where, "$."+term)
default:
where := "b.properties LIKE ?"
query = query.Where(where, "%\""+term+"\"%")
}
} else { // model.BoardSearchFieldTitle
// break search query into space separated words
// and search for all words.
// This should later be upgraded to industrial-strength
// word tokenizer, that uses much more than space
// to break words.
conditions := sq.And{}
for _, word := range strings.Split(strings.TrimSpace(term), " ") {
conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"})
}
query = query.Where(conditions)
}
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows)
}
// searchBoardsForUserInTeam returns all boards that match with the
// term that are either private and which the user is a member of, or
// they're open, regardless of the user membership.
// Search is case-insensitive.
func (s *SQLStore) searchBoardsForUserInTeam(db sq.BaseRunner, teamID, term, userID string) ([]*model.Board, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
Distinct().
From(s.tablePrefix + "boards as b").
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
Where(sq.Eq{"b.is_template": false}).
Where(sq.Eq{"b.team_id": teamID}).
Where(sq.Or{
sq.Eq{"b.type": model.BoardTypeOpen},
sq.And{
sq.Eq{"b.type": model.BoardTypePrivate},
sq.Eq{"bm.user_id": userID},
},
})
if term != "" {
// break search query into space separated words
// and search for all words.
// This should later be upgraded to industrial-strength
// word tokenizer, that uses much more than space
// to break words.
conditions := sq.And{}
for _, word := range strings.Split(strings.TrimSpace(term), " ") {
conditions = append(conditions, sq.Like{"lower(b.title)": "%" + strings.ToLower(word) + "%"})
}
query = query.Where(conditions)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`searchBoardsForUser ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows)
}
func (s *SQLStore) getBoardHistory(db sq.BaseRunner, boardID string, opts model.QueryBoardHistoryOptions) ([]*model.Board, error) {
var order string
if opts.Descending {
order = " DESC "
}
query := s.getQueryBuilder(db).
Select(boardHistoryFields()...).
From(s.tablePrefix + "boards_history").
Where(sq.Eq{"id": boardID}).
OrderBy("insert_at " + order + ", update_at" + order)
if opts.BeforeUpdateAt != 0 {
query = query.Where(sq.Lt{"update_at": opts.BeforeUpdateAt})
}
if opts.AfterUpdateAt != 0 {
query = query.Where(sq.Gt{"update_at": opts.AfterUpdateAt})
}
if opts.Limit != 0 {
query = query.Limit(opts.Limit)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardHistory ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.boardsFromRows(rows)
}
func (s *SQLStore) undeleteBoard(db sq.BaseRunner, boardID string, modifiedBy string) error {
boards, err := s.getBoardHistory(db, boardID, model.QueryBoardHistoryOptions{Limit: 1, Descending: true})
if err != nil {
return err
}
if len(boards) == 0 {
s.logger.Warn("undeleteBlock board not found", mlog.String("board_id", boardID))
return nil // undeleting non-existing board is not considered an error (for now)
}
board := boards[0]
if board.DeleteAt == 0 {
s.logger.Warn("undeleteBlock board not deleted", mlog.String("board_id", board.ID))
return nil // undeleting not deleted board is not considered an error (for now)
}
propertiesJSON, err := s.MarshalJSONB(board.Properties)
if err != nil {
return err
}
cardPropertiesJSON, err := s.MarshalJSONB(board.CardProperties)
if err != nil {
return err
}
now := utils.GetMillis()
columns := []string{
"id",
"team_id",
"channel_id",
"created_by",
"modified_by",
"type",
"title",
"minimum_role",
"description",
"icon",
"show_description",
"is_template",
"template_version",
"properties",
"card_properties",
"create_at",
"update_at",
"delete_at",
}
values := []interface{}{
board.ID,
board.TeamID,
"",
board.CreatedBy,
modifiedBy,
board.Type,
board.Title,
board.MinimumRole,
board.Description,
board.Icon,
board.ShowDescription,
board.IsTemplate,
board.TemplateVersion,
propertiesJSON,
cardPropertiesJSON,
board.CreateAt,
now,
0,
}
insertHistoryQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "boards_history").
Columns(columns...).
Values(values...)
insertQuery := s.getQueryBuilder(db).Insert(s.tablePrefix + "boards").
Columns(columns...).
Values(values...)
if _, err := insertHistoryQuery.Exec(); err != nil {
return err
}
if _, err := insertQuery.Exec(); err != nil {
return err
}
return s.undeleteBlockChildren(db, board.ID, "", modifiedBy)
}
func (s *SQLStore) getBoardMemberHistory(db sq.BaseRunner, boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error) {
query := s.getQueryBuilder(db).
Select("board_id", "user_id", "action", "insert_at").
From(s.tablePrefix + "board_members_history").
Where(sq.Eq{"board_id": boardID}).
Where(sq.Eq{"user_id": userID}).
OrderBy("insert_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`getBoardMemberHistory ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
memberHistory, err := s.boardMemberHistoryEntriesFromRows(rows)
if err != nil {
return nil, err
}
return memberHistory, nil
}

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

@@ -0,0 +1,154 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
sq "github.com/Masterminds/squirrel"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) getTeamBoardsInsights(db sq.BaseRunner, teamID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
boardsHistoryQuery := s.getQueryBuilder(db).
Select("boards.id, boards.icon, boards.title, count(boards_history.id) as count, boards_history.modified_by, boards.created_by").
From(s.tablePrefix + "boards_history as boards_history").
Join(s.tablePrefix + "boards as boards on boards_history.id = boards.id").
Where(sq.Gt{"boards_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}).
Where(sq.Eq{"boards.team_id": teamID}).
Where(sq.Eq{"boards.id": boardIDs}).
Where(sq.NotEq{"boards_history.modified_by": "system"}).
Where(sq.Eq{"boards.delete_at": 0}).
GroupBy("boards.id, boards_history.id, boards_history.modified_by")
blocksHistoryQuery := s.getQueryBuilder(db).
Select("boards.id, boards.icon, boards.title, count(blocks_history.id) as count, blocks_history.modified_by, boards.created_by").
Prefix("UNION ALL").
From(s.tablePrefix + "blocks_history as blocks_history").
Join(s.tablePrefix + "boards as boards on blocks_history.board_id = boards.id").
Where(sq.Gt{"blocks_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}).
Where(sq.Eq{"boards.team_id": teamID}).
Where(sq.Eq{"boards.id": boardIDs}).
Where(sq.NotEq{"blocks_history.modified_by": "system"}).
Where(sq.Eq{"boards.delete_at": 0}).
GroupBy("boards.id, blocks_history.board_id, blocks_history.modified_by")
boardsActivity := boardsHistoryQuery.SuffixExpr(blocksHistoryQuery)
insightsQuery := s.getQueryBuilder(db).Select(
fmt.Sprintf("id, title, icon, sum(count) as activity_count, %s as active_users, created_by", s.concatenationSelector("distinct modified_by", ",")),
).
FromSelect(boardsActivity, "boards_and_blocks_history").
GroupBy("id, title, icon, created_by").
OrderBy("activity_count desc").
Offset(uint64(offset)).
Limit(uint64(limit))
rows, err := insightsQuery.Query()
if err != nil {
s.logger.Error(`Team insights query ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
boardsInsights, err := boardsInsightsFromRows(rows)
if err != nil {
return nil, err
}
boardInsightsPaginated := model.GetTopBoardInsightsListWithPagination(boardsInsights, limit)
return boardInsightsPaginated, nil
}
func (s *SQLStore) getUserBoardsInsights(db sq.BaseRunner, teamID string, userID string, since int64, offset int, limit int, boardIDs []string) (*model.BoardInsightsList, error) {
boardsHistoryQuery := s.getQueryBuilder(db).
Select("boards.id, boards.icon, boards.title, count(boards_history.id) as count, boards_history.modified_by, boards.created_by").
From(s.tablePrefix + "boards_history as boards_history").
Join(s.tablePrefix + "boards as boards on boards_history.id = boards.id").
Where(sq.Gt{"boards_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}).
Where(sq.Eq{"boards.team_id": teamID}).
Where(sq.Eq{"boards.id": boardIDs}).
Where(sq.NotEq{"boards_history.modified_by": "system"}).
Where(sq.Eq{"boards.delete_at": 0}).
GroupBy("boards.id, boards_history.id, boards_history.modified_by")
blocksHistoryQuery := s.getQueryBuilder(db).
Select("boards.id, boards.icon, boards.title, count(blocks_history.id) as count, blocks_history.modified_by, boards.created_by").
Prefix("UNION ALL").
From(s.tablePrefix + "blocks_history as blocks_history").
Join(s.tablePrefix + "boards as boards on blocks_history.board_id = boards.id").
Where(sq.Gt{"blocks_history.insert_at": mm_model.GetTimeForMillis(since).Format(time.RFC3339)}).
Where(sq.Eq{"boards.team_id": teamID}).
Where(sq.Eq{"boards.id": boardIDs}).
Where(sq.NotEq{"blocks_history.modified_by": "system"}).
Where(sq.Eq{"boards.delete_at": 0}).
GroupBy("boards.id, blocks_history.board_id, blocks_history.modified_by")
boardsActivity := boardsHistoryQuery.SuffixExpr(blocksHistoryQuery)
insightsQuery := s.getQueryBuilder(db).Select(
fmt.Sprintf("id, title, icon, sum(count) as activity_count, %s as active_users, created_by", s.concatenationSelector("distinct modified_by", ",")),
).
FromSelect(boardsActivity, "boards_and_blocks_history").
GroupBy("id, title, icon, created_by").
OrderBy("activity_count desc")
userQuery := s.getQueryBuilder(db).Select("*").
FromSelect(insightsQuery, "boards_and_blocks_history_for_user").
Where(sq.Or{
sq.Eq{
"created_by": userID,
},
sq.Expr(s.elementInColumn("active_users"), userID),
}).
Offset(uint64(offset)).
Limit(uint64(limit))
rows, err := userQuery.Query()
if err != nil {
s.logger.Error(`Team insights query ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
boardsInsights, err := boardsInsightsFromRows(rows)
if err != nil {
return nil, err
}
boardInsightsPaginated := model.GetTopBoardInsightsListWithPagination(boardsInsights, limit)
return boardInsightsPaginated, nil
}
func boardsInsightsFromRows(rows *sql.Rows) ([]*model.BoardInsight, error) {
boardsInsights := []*model.BoardInsight{}
for rows.Next() {
var boardInsight model.BoardInsight
var activeUsersString string
err := rows.Scan(
&boardInsight.BoardID,
&boardInsight.Title,
&boardInsight.Icon,
&boardInsight.ActivityCount,
&activeUsersString,
&boardInsight.CreatedBy,
)
// split activeUsersString into slice
boardInsight.ActiveUsers = strings.Split(activeUsersString, ",")
if err != nil {
return nil, err
}
boardsInsights = append(boardsInsights, &boardInsight)
}
return boardsInsights, nil
}

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

@@ -0,0 +1,187 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
type BlockDoesntBelongToBoardsErr struct {
blockID string
}
func (e BlockDoesntBelongToBoardsErr) Error() string {
return fmt.Sprintf("block %s doesn't belong to any of the boards in the delete request", e.blockID)
}
func (s *SQLStore) createBoardsAndBlocksWithAdmin(db sq.BaseRunner, bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
newBab, err := s.createBoardsAndBlocks(db, bab, userID)
if err != nil {
return nil, nil, err
}
members := []*model.BoardMember{}
for _, board := range newBab.Boards {
bm := &model.BoardMember{
BoardID: board.ID,
UserID: board.CreatedBy,
SchemeAdmin: true,
SchemeEditor: true,
}
nbm, err := s.saveMember(db, bm)
if err != nil {
return nil, nil, err
}
members = append(members, nbm)
}
return newBab, members, nil
}
func (s *SQLStore) createBoardsAndBlocks(db sq.BaseRunner, bab *model.BoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
boards := []*model.Board{}
blocks := []*model.Block{}
for _, board := range bab.Boards {
newBoard, err := s.insertBoard(db, board, userID)
if err != nil {
return nil, err
}
boards = append(boards, newBoard)
}
for _, block := range bab.Blocks {
b := block
err := s.insertBlock(db, b, userID)
if err != nil {
return nil, err
}
blocks = append(blocks, block)
}
newBab := &model.BoardsAndBlocks{
Boards: boards,
Blocks: blocks,
}
return newBab, nil
}
func (s *SQLStore) patchBoardsAndBlocks(db sq.BaseRunner, pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
bab := &model.BoardsAndBlocks{}
for i, boardID := range pbab.BoardIDs {
board, err := s.patchBoard(db, boardID, pbab.BoardPatches[i], userID)
if err != nil {
return nil, err
}
bab.Boards = append(bab.Boards, board)
}
for i, blockID := range pbab.BlockIDs {
if err := s.patchBlock(db, blockID, pbab.BlockPatches[i], userID); err != nil {
return nil, err
}
block, err := s.getBlock(db, blockID)
if err != nil {
return nil, err
}
bab.Blocks = append(bab.Blocks, block)
}
return bab, nil
}
// deleteBoardsAndBlocks deletes all the boards and blocks entities of
// the DeleteBoardsAndBlocks struct, making sure that all the blocks
// belong to the boards in the struct.
func (s *SQLStore) deleteBoardsAndBlocks(db sq.BaseRunner, dbab *model.DeleteBoardsAndBlocks, userID string) error {
boardIDMap := map[string]bool{}
for _, boardID := range dbab.Boards {
boardIDMap[boardID] = true
}
// delete the blocks first, since deleting the board will clean up any children and we'll get
// not found errors when deleting the blocks after.
for _, blockID := range dbab.Blocks {
block, err := s.getBlock(db, blockID)
if err != nil {
return err
}
if _, ok := boardIDMap[block.BoardID]; !ok {
return BlockDoesntBelongToBoardsErr{blockID}
}
if err := s.deleteBlock(db, blockID, userID); err != nil {
return err
}
}
for _, boardID := range dbab.Boards {
if err := s.deleteBoard(db, boardID, userID); err != nil {
return err
}
}
return nil
}
func (s *SQLStore) duplicateBoard(db sq.BaseRunner, boardID string, userID string, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
bab := &model.BoardsAndBlocks{
Boards: []*model.Board{},
Blocks: []*model.Block{},
}
board, err := s.getBoard(db, boardID)
if err != nil {
return nil, nil, err
}
// todo: server localization
if asTemplate == board.IsTemplate {
// board -> board or template -> template
board.Title += " copy"
} else if asTemplate {
// template from board
board.Title = "New board template"
}
// make new board private
board.Type = "P"
board.IsTemplate = asTemplate
board.CreatedBy = userID
board.ChannelID = ""
if toTeam != "" {
board.TeamID = toTeam
}
bab.Boards = []*model.Board{board}
blocks, err := s.getBlocksForBoard(db, boardID)
if err != nil {
return nil, nil, err
}
newBlocks := []*model.Block{}
for _, b := range blocks {
if b.Type != model.TypeComment {
newBlocks = append(newBlocks, b)
}
}
bab.Blocks = newBlocks
bab, err = model.GenerateBoardsAndBlocksIDs(bab, nil)
if err != nil {
return nil, nil, err
}
return s.createBoardsAndBlocksWithAdmin(db, bab, userID)
}

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

@@ -0,0 +1,255 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"bytes"
"context"
"database/sql"
"fmt"
"path/filepath"
"text/template"
"github.com/mattermost/morph"
"github.com/mattermost/morph/drivers"
"github.com/mattermost/morph/drivers/mysql"
"github.com/mattermost/morph/drivers/postgres"
embedded "github.com/mattermost/morph/sources/embedded"
"github.com/mgdelacroix/foundation"
"github.com/mattermost/mattermost-server/v6/server/channels/db"
mmSqlStore "github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
var tablePrefix = "focalboard_"
type BoardsMigrator struct {
connString string
driverName string
db *sql.DB
store *SQLStore
morphEngine *morph.Morph
morphDriver drivers.Driver
}
func NewBoardsMigrator(store *SQLStore) *BoardsMigrator {
return &BoardsMigrator{
connString: store.connectionString,
driverName: store.dbType,
store: store,
}
}
func (bm *BoardsMigrator) runMattermostMigrations() error {
assets := db.Assets()
assetsList, err := assets.ReadDir(filepath.Join("migrations", bm.driverName))
if err != nil {
return err
}
assetNames := make([]string, len(assetsList))
for i, entry := range assetsList {
assetNames[i] = entry.Name()
}
src, err := embedded.WithInstance(&embedded.AssetSource{
Names: assetNames,
AssetFunc: func(name string) ([]byte, error) {
return assets.ReadFile(filepath.Join("migrations", bm.driverName, name))
},
})
if err != nil {
return err
}
driver, err := bm.getDriver()
if err != nil {
return err
}
options := []morph.EngineOption{
morph.SetStatementTimeoutInSeconds(1000000),
}
engine, err := morph.New(context.Background(), driver, src, options...)
if err != nil {
return err
}
defer engine.Close()
return engine.ApplyAll()
}
func (bm *BoardsMigrator) getDriver() (drivers.Driver, error) {
var driver drivers.Driver
var err error
switch bm.driverName {
case model.PostgresDBType:
driver, err = postgres.WithInstance(bm.db)
if err != nil {
return nil, err
}
case model.MysqlDBType:
driver, err = mysql.WithInstance(bm.db)
if err != nil {
return nil, err
}
}
return driver, nil
}
func (bm *BoardsMigrator) getMorphConnection() (*morph.Morph, drivers.Driver, error) {
driver, err := bm.getDriver()
if err != nil {
return nil, nil, err
}
assetsList, err := Assets.ReadDir("migrations")
if err != nil {
return nil, nil, err
}
assetNamesForDriver := make([]string, len(assetsList))
for i, dirEntry := range assetsList {
assetNamesForDriver[i] = dirEntry.Name()
}
params := map[string]interface{}{
"prefix": tablePrefix,
"postgres": bm.driverName == model.PostgresDBType,
"mysql": bm.driverName == model.MysqlDBType,
"plugin": true, // TODO: to be removed
"singleUser": false,
}
migrationAssets := &embedded.AssetSource{
Names: assetNamesForDriver,
AssetFunc: func(name string) ([]byte, error) {
asset, mErr := Assets.ReadFile("migrations/" + name)
if mErr != nil {
return nil, mErr
}
tmpl, pErr := template.New("sql").Funcs(bm.store.GetTemplateHelperFuncs()).Parse(string(asset))
if pErr != nil {
return nil, pErr
}
buffer := bytes.NewBufferString("")
err = tmpl.Execute(buffer, params)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
},
}
src, err := embedded.WithInstance(migrationAssets)
if err != nil {
return nil, nil, err
}
engine, err := morph.New(context.Background(), driver, src, morph.SetMigrationTableName(fmt.Sprintf("%sschema_migrations", tablePrefix)))
if err != nil {
return nil, nil, err
}
return engine, driver, nil
}
func (bm *BoardsMigrator) Setup() error {
var err error
if bm.driverName == model.MysqlDBType {
bm.connString, err = mmSqlStore.ResetReadTimeout(bm.connString)
if err != nil {
return err
}
bm.connString, err = mmSqlStore.AppendMultipleStatementsFlag(bm.connString)
if err != nil {
return err
}
}
var dbErr error
bm.db, dbErr = sql.Open(bm.driverName, bm.connString)
if dbErr != nil {
return dbErr
}
if err2 := bm.db.Ping(); err2 != nil {
return err2
}
if err3 := bm.runMattermostMigrations(); err3 != nil {
return err3
}
storeParams := Params{
DBType: bm.driverName,
ConnectionString: bm.connString,
TablePrefix: tablePrefix,
Logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug),
DB: bm.db,
IsPlugin: true, // TODO: to be removed
SkipMigrations: true,
}
bm.store, err = New(storeParams)
if err != nil {
return err
}
morphEngine, morphDriver, err := bm.getMorphConnection()
if err != nil {
return err
}
bm.morphEngine = morphEngine
bm.morphDriver = morphDriver
return nil
}
func (bm *BoardsMigrator) MigrateToStep(step int) error {
applied, err := bm.morphDriver.AppliedMigrations()
if err != nil {
return err
}
currentVersion := len(applied)
if _, err := bm.morphEngine.Apply(step - currentVersion); err != nil {
return err
}
return nil
}
func (bm *BoardsMigrator) Interceptors() map[int]foundation.Interceptor {
return map[int]foundation.Interceptor{
18: bm.store.RunDeletedMembershipBoardsMigration,
}
}
func (bm *BoardsMigrator) TearDown() error {
if err := bm.morphEngine.Close(); err != nil {
return err
}
if err := bm.db.Close(); err != nil {
return err
}
return nil
}
func (bm *BoardsMigrator) DriverName() string {
return bm.driverName
}
func (bm *BoardsMigrator) DB() *sql.DB {
return bm.db
}

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

@@ -0,0 +1,249 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const categorySortOrderGap = 10
func (s *SQLStore) categoryFields() []string {
return []string{
"id",
"name",
"user_id",
"team_id",
"create_at",
"update_at",
"delete_at",
"collapsed",
"COALESCE(sort_order, 0)",
"type",
}
}
func (s *SQLStore) getCategory(db sq.BaseRunner, id string) (*model.Category, error) {
query := s.getQueryBuilder(db).
Select(s.categoryFields()...).
From(s.tablePrefix + "categories").
Where(sq.Eq{"id": id})
rows, err := query.Query()
if err != nil {
s.logger.Error("getCategory error", mlog.Err(err))
return nil, err
}
categories, err := s.categoriesFromRows(rows)
if err != nil {
s.logger.Error("getCategory row scan error", mlog.Err(err))
return nil, err
}
if len(categories) == 0 {
return nil, model.NewErrNotFound("category ID=" + id)
}
return &categories[0], nil
}
func (s *SQLStore) createCategory(db sq.BaseRunner, category model.Category) error {
// A new category should always end up at the top.
// So we first insert the provided category, then bump up
// existing user-team categories' order
// creating provided category
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"categories").
Columns(
"id",
"name",
"user_id",
"team_id",
"create_at",
"update_at",
"delete_at",
"collapsed",
"sort_order",
"type",
).
Values(
category.ID,
category.Name,
category.UserID,
category.TeamID,
category.CreateAt,
category.UpdateAt,
category.DeleteAt,
category.Collapsed,
category.SortOrder,
category.Type,
)
_, err := query.Exec()
if err != nil {
s.logger.Error("Error creating category", mlog.String("category name", category.Name), mlog.Err(err))
return err
}
// bumping up order of existing categories
updateQuery := s.getQueryBuilder(db).
Update(s.tablePrefix+"categories").
Set("sort_order", sq.Expr(fmt.Sprintf("sort_order + %d", categorySortOrderGap))).
Where(
sq.Eq{
"user_id": category.UserID,
"team_id": category.TeamID,
"delete_at": 0,
},
)
if _, err := updateQuery.Exec(); err != nil {
s.logger.Error(
"createCategory failed to update sort order of existing user-team categories",
mlog.String("user_id", category.UserID),
mlog.String("team_id", category.TeamID),
mlog.Err(err),
)
return err
}
return nil
}
func (s *SQLStore) updateCategory(db sq.BaseRunner, category model.Category) error {
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"categories").
Set("name", category.Name).
Set("update_at", category.UpdateAt).
Set("collapsed", category.Collapsed).
Where(sq.Eq{
"id": category.ID,
"delete_at": 0,
})
_, err := query.Exec()
if err != nil {
s.logger.Error("Error updating category", mlog.String("category_id", category.ID), mlog.String("category_name", category.Name), mlog.Err(err))
return err
}
return nil
}
func (s *SQLStore) deleteCategory(db sq.BaseRunner, categoryID, userID, teamID string) error {
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"categories").
Set("delete_at", utils.GetMillis()).
Where(sq.Eq{
"id": categoryID,
"user_id": userID,
"team_id": teamID,
"delete_at": 0,
})
_, err := query.Exec()
if err != nil {
s.logger.Error(
"Error updating category",
mlog.String("category_id", categoryID),
mlog.String("user_id", userID),
mlog.String("team_id", teamID),
mlog.Err(err),
)
return err
}
return nil
}
func (s *SQLStore) getUserCategories(db sq.BaseRunner, userID, teamID string) ([]model.Category, error) {
query := s.getQueryBuilder(db).
Select(s.categoryFields()...).
From(s.tablePrefix+"categories").
Where(sq.Eq{
"user_id": userID,
"team_id": teamID,
"delete_at": 0,
}).
OrderBy("sort_order", "name")
rows, err := query.Query()
if err != nil {
s.logger.Error("getUserCategories error", mlog.Err(err))
return nil, err
}
return s.categoriesFromRows(rows)
}
func (s *SQLStore) categoriesFromRows(rows *sql.Rows) ([]model.Category, error) {
var categories []model.Category
for rows.Next() {
category := model.Category{}
err := rows.Scan(
&category.ID,
&category.Name,
&category.UserID,
&category.TeamID,
&category.CreateAt,
&category.UpdateAt,
&category.DeleteAt,
&category.Collapsed,
&category.SortOrder,
&category.Type,
)
if err != nil {
s.logger.Error("categoriesFromRows row parsing error", mlog.Err(err))
return nil, err
}
categories = append(categories, category)
}
return categories, nil
}
func (s *SQLStore) reorderCategories(db sq.BaseRunner, userID, teamID string, newCategoryOrder []string) ([]string, error) {
if len(newCategoryOrder) == 0 {
return nil, nil
}
updateCase := sq.Case("id")
for i, categoryID := range newCategoryOrder {
updateCase = updateCase.When("'"+categoryID+"'", sq.Expr(fmt.Sprintf("%d", i*categorySortOrderGap)))
}
updateCase = updateCase.Else("sort_order")
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"categories").
Set("sort_order", updateCase).
Where(sq.Eq{
"user_id": userID,
"team_id": teamID,
})
if _, err := query.Exec(); err != nil {
s.logger.Error(
"reorderCategories failed to update category order",
mlog.String("user_id", userID),
mlog.String("team_id", teamID),
mlog.Err(err),
)
return nil, err
}
return newCategoryOrder, nil
}

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

@@ -0,0 +1,195 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) getUserCategoryBoards(db sq.BaseRunner, userID, teamID string) ([]model.CategoryBoards, error) {
categories, err := s.getUserCategories(db, userID, teamID)
if err != nil {
return nil, err
}
userCategoryBoards := []model.CategoryBoards{}
for _, category := range categories {
boardMetadata, err := s.getCategoryBoardAttributes(db, category.ID)
if err != nil {
return nil, err
}
userCategoryBoard := model.CategoryBoards{
Category: category,
BoardMetadata: boardMetadata,
}
userCategoryBoards = append(userCategoryBoards, userCategoryBoard)
}
return userCategoryBoards, nil
}
func (s *SQLStore) getCategoryBoardAttributes(db sq.BaseRunner, categoryID string) ([]model.CategoryBoardMetadata, error) {
query := s.getQueryBuilder(db).
Select("board_id, COALESCE(hidden, false)").
From(s.tablePrefix + "category_boards").
Where(sq.Eq{
"category_id": categoryID,
}).
OrderBy("sort_order")
rows, err := query.Query()
if err != nil {
s.logger.Error("getCategoryBoards error fetching categoryblocks", mlog.String("categoryID", categoryID), mlog.Err(err))
return nil, err
}
return s.categoryBoardsFromRows(rows)
}
func (s *SQLStore) addUpdateCategoryBoard(db sq.BaseRunner, userID, categoryID string, boardIDsParam []string) error {
// we need to de-duplicate this array as Postgres failes to
// handle upsert if there are multiple incoming rows
// that conflict the same existing row.
// For example, having the entry "1" in DB and trying to upsert "1" and "1" will fail
// as there are multiple duplicates of the same "1".
//
// Source: https://stackoverflow.com/questions/42994373/postgresql-on-conflict-cannot-affect-row-a-second-time
boardIDs := utils.DedupeStringArr(boardIDsParam)
if len(boardIDs) == 0 {
return nil
}
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"category_boards").
Columns(
"id",
"user_id",
"category_id",
"board_id",
"create_at",
"update_at",
"sort_order",
"hidden",
)
now := utils.GetMillis()
for _, boardID := range boardIDs {
query = query.Values(
utils.NewID(utils.IDTypeNone),
userID,
categoryID,
boardID,
now,
now,
0,
false,
)
}
if s.dbType == model.MysqlDBType {
query = query.Suffix(
"ON DUPLICATE KEY UPDATE category_id = ?",
categoryID,
)
} else {
query = query.Suffix(
`ON CONFLICT (user_id, board_id)
DO UPDATE SET category_id = EXCLUDED.category_id, update_at = EXCLUDED.update_at`,
)
}
if _, err := query.Exec(); err != nil {
return fmt.Errorf(
"store addUpdateCategoryBoard: failed to upsert user-board-category userID: %s, categoryID: %s, board_count: %d, error: %w",
userID, categoryID, len(boardIDs), err,
)
}
return nil
}
func (s *SQLStore) categoryBoardsFromRows(rows *sql.Rows) ([]model.CategoryBoardMetadata, error) {
metadata := []model.CategoryBoardMetadata{}
for rows.Next() {
datum := model.CategoryBoardMetadata{}
err := rows.Scan(&datum.BoardID, &datum.Hidden)
if err != nil {
s.logger.Error("categoryBoardsFromRows row scan error", mlog.Err(err))
return nil, err
}
metadata = append(metadata, datum)
}
return metadata, nil
}
func (s *SQLStore) reorderCategoryBoards(db sq.BaseRunner, categoryID string, newBoardsOrder []string) ([]string, error) {
if len(newBoardsOrder) == 0 {
return nil, nil
}
updateCase := sq.Case("board_id")
for i, boardID := range newBoardsOrder {
updateCase = updateCase.When("'"+boardID+"'", sq.Expr(fmt.Sprintf("%d", i+model.CategoryBoardsSortOrderGap)))
}
updateCase.Else("sort_order")
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"category_boards").
Set("sort_order", updateCase).
Where(sq.Eq{
"category_id": categoryID,
})
if _, err := query.Exec(); err != nil {
s.logger.Error(
"reorderCategoryBoards failed to update category board order",
mlog.String("category_id", categoryID),
mlog.Err(err),
)
return nil, err
}
return newBoardsOrder, nil
}
func (s *SQLStore) setBoardVisibility(db sq.BaseRunner, userID, categoryID, boardID string, visible bool) error {
query := s.getQueryBuilder(db).
Update(s.tablePrefix+"category_boards").
Set("hidden", !visible).
Where(sq.Eq{
"user_id": userID,
"category_id": categoryID,
"board_id": boardID,
})
if _, err := query.Exec(); err != nil {
s.logger.Error(
"SQLStore setBoardVisibility: failed to update board visibility",
mlog.String("user_id", userID),
mlog.String("board_id", boardID),
mlog.Bool("visible", visible),
mlog.Err(err),
)
return err
}
return nil
}

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

@@ -0,0 +1,118 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"errors"
"strconv"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store"
)
var ErrInvalidCardLimitValue = errors.New("card limit value is invalid")
// activeCardsQuery applies the necessary filters to the query for it
// to fetch an active cards window if the cardLimit is set, or all the
// active cards if it's 0.
func (s *SQLStore) activeCardsQuery(builder sq.StatementBuilderType, selectStr string, cardLimit int) sq.SelectBuilder {
query := builder.
Select(selectStr).
From(s.tablePrefix + "blocks b").
Join(s.tablePrefix + "boards bd on b.board_id=bd.id").
Where(sq.Eq{
"b.delete_at": 0,
"b.type": model.TypeCard,
"bd.is_template": false,
})
if cardLimit != 0 {
query = query.
Limit(1).
Offset(uint64(cardLimit - 1))
}
return query
}
// getUsedCardsCount returns the amount of active cards in the server.
func (s *SQLStore) getUsedCardsCount(db sq.BaseRunner) (int, error) {
row := s.activeCardsQuery(s.getQueryBuilder(db), "count(b.id)", 0).
QueryRow()
var usedCards int
err := row.Scan(&usedCards)
if err != nil {
return 0, err
}
return usedCards, nil
}
// getCardLimitTimestamp returns the timestamp value from the
// system_settings table or zero if it doesn't exist.
func (s *SQLStore) getCardLimitTimestamp(db sq.BaseRunner) (int64, error) {
scanner := s.getQueryBuilder(db).
Select("value").
From(s.tablePrefix + "system_settings").
Where(sq.Eq{"id": store.CardLimitTimestampSystemKey}).
QueryRow()
var result string
err := scanner.Scan(&result)
if errors.Is(sql.ErrNoRows, err) {
return 0, nil
}
if err != nil {
return 0, err
}
cardLimitTimestamp, err := strconv.Atoi(result)
if err != nil {
return 0, ErrInvalidCardLimitValue
}
return int64(cardLimitTimestamp), nil
}
// updateCardLimitTimestamp updates the card limit value in the
// system_settings table with the timestamp of the nth last updated
// card, being nth the value of the cardLimit parameter. If cardLimit
// is zero, the timestamp will be set to zero.
func (s *SQLStore) updateCardLimitTimestamp(db sq.BaseRunner, cardLimit int) (int64, error) {
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"system_settings").
Columns("id", "value")
var value interface{} = 0
if cardLimit != 0 {
value = s.activeCardsQuery(sq.StatementBuilder, "b.update_at", cardLimit).
OrderBy("b.update_at DESC").
Prefix("COALESCE((").Suffix("), 0)")
}
query = query.Values(store.CardLimitTimestampSystemKey, value)
if s.dbType == model.MysqlDBType {
query = query.Suffix("ON DUPLICATE KEY UPDATE value = ?", value)
} else {
query = query.Suffix(
`ON CONFLICT (id)
DO UPDATE SET value = EXCLUDED.value`,
)
}
result, err := query.Exec()
if err != nil {
return 0, err
}
if _, err := result.RowsAffected(); err != nil {
return 0, err
}
return s.getCardLimitTimestamp(db)
}

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

@@ -0,0 +1,245 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) getBoardsForCompliance(db sq.BaseRunner, opts model.QueryBoardsForComplianceOptions) ([]*model.Board, bool, error) {
query := s.getQueryBuilder(db).
Select(boardFields("b.")...).
From(s.tablePrefix + "boards as b")
if opts.TeamID != "" {
query = query.Where(sq.Eq{"b.team_id": opts.TeamID})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// N+1 to check if there's a next page for pagination
query = query.Limit(uint64(opts.PerPage) + 1)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBoardsForCompliance ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, false, err
}
var hasMore bool
if opts.PerPage > 0 && len(boards) > opts.PerPage {
boards = boards[0:opts.PerPage]
hasMore = true
}
return boards, hasMore, nil
}
func (s *SQLStore) getBoardsComplianceHistory(db sq.BaseRunner, opts model.QueryBoardsComplianceHistoryOptions) ([]*model.BoardHistory, bool, error) {
queryDescendentLastUpdate := s.getQueryBuilder(db).
Select("MAX(blk1.update_at)").
From(s.tablePrefix + "blocks_history as blk1").
Where("blk1.board_id=bh.id")
if !opts.IncludeDeleted {
queryDescendentLastUpdate.Where(sq.Eq{"blk1.delete_at": 0})
}
sqlDescendentLastUpdate, _, _ := queryDescendentLastUpdate.ToSql()
queryDescendentFirstUpdate := s.getQueryBuilder(db).
Select("MIN(blk2.update_at)").
From(s.tablePrefix + "blocks_history as blk2").
Where("blk2.board_id=bh.id")
if !opts.IncludeDeleted {
queryDescendentFirstUpdate.Where(sq.Eq{"blk2.delete_at": 0})
}
sqlDescendentFirstUpdate, _, _ := queryDescendentFirstUpdate.ToSql()
query := s.getQueryBuilder(db).
Select(
"bh.id",
"bh.team_id",
"CASE WHEN bh.delete_at=0 THEN false ELSE true END AS isDeleted",
"COALESCE(("+sqlDescendentLastUpdate+"),0) as decendentLastUpdateAt",
"COALESCE(("+sqlDescendentFirstUpdate+"),0) as decendentFirstUpdateAt",
"bh.created_by",
"bh.modified_by",
).
From(s.tablePrefix + "boards_history as bh")
if !opts.IncludeDeleted {
// filtering out deleted boards; join with boards table to ensure no history
// for deleted boards are returned. Deleted boards won't exist in boards table.
query = query.Join(s.tablePrefix + "boards as b ON b.id=bh.id")
}
query = query.Where(sq.Gt{"bh.update_at": opts.ModifiedSince}).
GroupBy("bh.id", "bh.team_id", "bh.delete_at", "bh.created_by", "bh.modified_by").
OrderBy("decendentLastUpdateAt desc", "bh.id")
if opts.TeamID != "" {
query = query.Where(sq.Eq{"bh.team_id": opts.TeamID})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// N+1 to check if there's a next page for pagination
query = query.Limit(uint64(opts.PerPage) + 1)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBoardsComplianceHistory ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
history, err := s.boardsHistoryFromRows(rows)
if err != nil {
return nil, false, err
}
var hasMore bool
if opts.PerPage > 0 && len(history) > opts.PerPage {
history = history[0:opts.PerPage]
hasMore = true
}
return history, hasMore, nil
}
func (s *SQLStore) getBlocksComplianceHistory(db sq.BaseRunner, opts model.QueryBlocksComplianceHistoryOptions) ([]*model.BlockHistory, bool, error) {
query := s.getQueryBuilder(db).
Select(
"bh.id",
"brd.team_id",
"bh.board_id",
"bh.type",
"CASE WHEN bh.delete_at=0 THEN false ELSE true END AS isDeleted",
"max(bh.update_at) as lastUpdateAt",
"min(bh.update_at) as firstUpdateAt",
"bh.created_by",
"bh.modified_by",
).
From(s.tablePrefix + "blocks_history as bh").
Join(s.tablePrefix + "boards_history as brd on brd.id=bh.board_id")
if !opts.IncludeDeleted {
// filtering out deleted blocks; join with blocks table to ensure no history
// for deleted blocks are returned. Deleted blocks won't exist in blocks table.
query = query.Join(s.tablePrefix + "blocks as b ON b.id=bh.id")
}
query = query.Where(sq.Gt{"bh.update_at": opts.ModifiedSince}).
GroupBy("bh.id", "brd.team_id", "bh.board_id", "bh.type", "bh.delete_at", "bh.created_by", "bh.modified_by").
OrderBy("lastUpdateAt desc", "bh.id")
if opts.TeamID != "" {
query = query.Where(sq.Eq{"brd.team_id": opts.TeamID})
}
if opts.BoardID != "" {
query = query.Where(sq.Eq{"bh.board_id": opts.BoardID})
}
if opts.Page != 0 {
query = query.Offset(uint64(opts.Page * opts.PerPage))
}
if opts.PerPage > 0 {
// N+1 to check if there's a next page for pagination
query = query.Limit(uint64(opts.PerPage) + 1)
}
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlocksComplianceHistory ERROR`, mlog.Err(err))
return nil, false, err
}
defer s.CloseRows(rows)
history, err := s.blocksHistoryFromRows(rows)
if err != nil {
return nil, false, err
}
var hasMore bool
if opts.PerPage > 0 && len(history) > opts.PerPage {
history = history[0:opts.PerPage]
hasMore = true
}
return history, hasMore, nil
}
func (s *SQLStore) boardsHistoryFromRows(rows *sql.Rows) ([]*model.BoardHistory, error) {
history := []*model.BoardHistory{}
for rows.Next() {
boardHistory := &model.BoardHistory{}
err := rows.Scan(
&boardHistory.ID,
&boardHistory.TeamID,
&boardHistory.IsDeleted,
&boardHistory.DescendantLastUpdateAt,
&boardHistory.DescendantFirstUpdateAt,
&boardHistory.CreatedBy,
&boardHistory.LastModifiedBy,
)
if err != nil {
s.logger.Error("boardsHistoryFromRows scan error", mlog.Err(err))
return nil, err
}
history = append(history, boardHistory)
}
return history, nil
}
func (s *SQLStore) blocksHistoryFromRows(rows *sql.Rows) ([]*model.BlockHistory, error) {
history := []*model.BlockHistory{}
for rows.Next() {
blockHistory := &model.BlockHistory{}
err := rows.Scan(
&blockHistory.ID,
&blockHistory.TeamID,
&blockHistory.BoardID,
&blockHistory.Type,
&blockHistory.IsDeleted,
&blockHistory.LastUpdateAt,
&blockHistory.FirstUpdateAt,
&blockHistory.CreatedBy,
&blockHistory.LastModifiedBy,
)
if err != nil {
s.logger.Error("blocksHistoryFromRows scan error", mlog.Err(err))
return nil, err
}
history = append(history, blockHistory)
}
return history, nil
}

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

@@ -0,0 +1,887 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"context"
"fmt"
"os"
"strconv"
sq "github.com/Masterminds/squirrel"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
// we group the inserts on batches of 1000 because PostgreSQL
// supports a limit of around 64K values (not rows) on an insert
// query, so we want to stay safely below.
CategoryInsertBatch = 1000
TemplatesToTeamsMigrationKey = "TemplatesToTeamsMigrationComplete"
UniqueIDsMigrationKey = "UniqueIDsMigrationComplete"
CategoryUUIDIDMigrationKey = "CategoryUuidIdMigrationComplete"
TeamLessBoardsMigrationKey = "TeamLessBoardsMigrationComplete"
DeletedMembershipBoardsMigrationKey = "DeletedMembershipBoardsMigrationComplete"
DeDuplicateCategoryBoardTableMigrationKey = "DeDuplicateCategoryBoardTableComplete"
)
func (s *SQLStore) getBlocksWithSameID(db sq.BaseRunner) ([]*model.Block, error) {
subquery, _, _ := s.getQueryBuilder(db).
Select("id").
From(s.tablePrefix + "blocks").
Having("count(id) > 1").
GroupBy("id").
ToSql()
blocksFields := []string{
"id",
"parent_id",
"root_id",
"created_by",
"modified_by",
s.escapeField("schema"),
"type",
"title",
"COALESCE(fields, '{}')",
s.timestampToCharField("insert_at", "insertAt"),
"create_at",
"update_at",
"delete_at",
"COALESCE(workspace_id, '0')",
}
rows, err := s.getQueryBuilder(db).
Select(blocksFields...).
From(s.tablePrefix + "blocks").
Where(fmt.Sprintf("id IN (%s)", subquery)).
Query()
if err != nil {
s.logger.Error(`getBlocksWithSameID ERROR`, mlog.Err(err))
return nil, err
}
defer s.CloseRows(rows)
return s.blocksFromRows(rows)
}
func (s *SQLStore) RunUniqueIDsMigration() error {
setting, err := s.GetSystemSetting(UniqueIDsMigrationKey)
if err != nil {
return fmt.Errorf("cannot get migration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
s.logger.Debug("Running Unique IDs migration")
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
blocks, err := s.getBlocksWithSameID(tx)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "getBlocksWithSameID"))
}
return fmt.Errorf("cannot get blocks with same ID: %w", err)
}
blocksByID := map[string][]*model.Block{}
for _, block := range blocks {
blocksByID[block.ID] = append(blocksByID[block.ID], block)
}
for _, blocks := range blocksByID {
for i, block := range blocks {
if i == 0 {
// do nothing for the first ID, only updating the others
continue
}
newID := utils.NewID(model.BlockType2IDType(block.Type))
if err := s.replaceBlockID(tx, block.ID, newID, block.WorkspaceID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "replaceBlockID"))
}
return fmt.Errorf("cannot replace blockID %s: %w", block.ID, err)
}
}
}
if err := s.setSystemSetting(tx, UniqueIDsMigrationKey, strconv.FormatBool(true)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("Unique IDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting"))
}
return fmt.Errorf("cannot mark migration as completed: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("cannot commit unique IDs transaction: %w", err)
}
s.logger.Debug("Unique IDs migration finished successfully")
return nil
}
// RunCategoryUUIDIDMigration takes care of deriving the categories
// from the boards and its memberships. The name references UUID
// because of the preexisting purpose of this migration, and has been
// preserved for compatibility with already migrated instances.
func (s *SQLStore) RunCategoryUUIDIDMigration() error {
setting, err := s.GetSystemSetting(CategoryUUIDIDMigrationKey)
if err != nil {
return fmt.Errorf("cannot get migration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
s.logger.Debug("Running category UUID ID migration")
tx, txErr := s.db.BeginTx(context.Background(), nil)
if txErr != nil {
return txErr
}
if s.isPlugin {
if err := s.createCategories(tx); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("category UUIDs insert categories transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting"))
}
return err
}
if err := s.createCategoryBoards(tx); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("category UUIDs insert category boards transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting"))
}
return err
}
}
if err := s.setSystemSetting(tx, CategoryUUIDIDMigrationKey, strconv.FormatBool(true)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("category UUIDs transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "setSystemSetting"))
}
return fmt.Errorf("cannot mark migration as completed: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("cannot commit category UUIDs transaction: %w", err)
}
s.logger.Debug("category UUIDs migration finished successfully")
return nil
}
func (s *SQLStore) createCategories(db sq.BaseRunner) error {
rows, err := s.getQueryBuilder(db).
Select("c.DisplayName, cm.UserId, c.TeamId, cm.ChannelId").
From(s.tablePrefix + "boards boards").
Join("ChannelMembers cm on boards.channel_id = cm.ChannelId").
Join("Channels c on cm.ChannelId = c.id and (c.Type = 'O' or c.Type = 'P')").
GroupBy("cm.UserId, c.TeamId, cm.ChannelId, c.DisplayName").
Query()
if err != nil {
s.logger.Error("get boards data error", mlog.Err(err))
return err
}
defer s.CloseRows(rows)
initQuery := func() sq.InsertBuilder {
return s.getQueryBuilder(db).
Insert(s.tablePrefix+"categories").
Columns(
"id",
"name",
"user_id",
"team_id",
"channel_id",
"create_at",
"update_at",
"delete_at",
)
}
// query will accumulate the insert values until the limit is
// reached, and then it will be stored and reset
query := initQuery()
// queryList stores those queries that already reached the limit
// to be run when all the data is processed
queryList := []sq.InsertBuilder{}
counter := 0
now := model.GetMillis()
for rows.Next() {
var displayName string
var userID string
var teamID string
var channelID string
err := rows.Scan(
&displayName,
&userID,
&teamID,
&channelID,
)
if err != nil {
return fmt.Errorf("cannot scan result while trying to create categories: %w", err)
}
query = query.Values(
utils.NewID(utils.IDTypeNone),
displayName,
userID,
teamID,
channelID,
now,
0,
0,
)
counter++
if counter%CategoryInsertBatch == 0 {
queryList = append(queryList, query)
query = initQuery()
}
}
if counter%CategoryInsertBatch != 0 {
queryList = append(queryList, query)
}
for _, q := range queryList {
if _, err := q.Exec(); err != nil {
return fmt.Errorf("cannot create category values: %w", err)
}
}
return nil
}
func (s *SQLStore) createCategoryBoards(db sq.BaseRunner) error {
rows, err := s.getQueryBuilder(db).
Select("categories.user_id, categories.id, boards.id").
From(s.tablePrefix + "categories categories").
Join(s.tablePrefix + "boards boards on categories.channel_id = boards.channel_id AND boards.is_template = false").
Query()
if err != nil {
s.logger.Error("get categories data error", mlog.Err(err))
return err
}
defer s.CloseRows(rows)
initQuery := func() sq.InsertBuilder {
return s.getQueryBuilder(db).
Insert(s.tablePrefix+"category_boards").
Columns(
"id",
"user_id",
"category_id",
"board_id",
"create_at",
"update_at",
"delete_at",
)
}
// query will accumulate the insert values until the limit is
// reached, and then it will be stored and reset
query := initQuery()
// queryList stores those queries that already reached the limit
// to be run when all the data is processed
queryList := []sq.InsertBuilder{}
counter := 0
now := model.GetMillis()
for rows.Next() {
var userID string
var categoryID string
var boardID string
err := rows.Scan(
&userID,
&categoryID,
&boardID,
)
if err != nil {
return fmt.Errorf("cannot scan result while trying to create category boards: %w", err)
}
query = query.Values(
utils.NewID(utils.IDTypeNone),
userID,
categoryID,
boardID,
now,
0,
0,
)
counter++
if counter%CategoryInsertBatch == 0 {
queryList = append(queryList, query)
query = initQuery()
}
}
if counter%CategoryInsertBatch != 0 {
queryList = append(queryList, query)
}
for _, q := range queryList {
if _, err := q.Exec(); err != nil {
return fmt.Errorf("cannot create category boards values: %w", err)
}
}
return nil
}
// We no longer support boards existing in DMs and private
// group messages. This function migrates all boards
// belonging to a DM to the best possible team.
func (s *SQLStore) RunTeamLessBoardsMigration() error {
if !s.isPlugin {
return nil
}
setting, err := s.GetSystemSetting(TeamLessBoardsMigrationKey)
if err != nil {
return fmt.Errorf("cannot get teamless boards migration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
boards, err := s.getDMBoards(s.db)
if err != nil {
return err
}
s.logger.Debug("Migrating teamless boards to a team", mlog.Int("count", len(boards)))
// cache for best suitable team for a DM. Since a DM can
// contain multiple boards, caching this avoids
// duplicate queries for the same DM.
channelToTeamCache := map[string]string{}
tx, err := s.db.BeginTx(context.Background(), nil)
if err != nil {
s.logger.Error("error starting transaction in runTeamLessBoardsMigration", mlog.Err(err))
return err
}
for i := range boards {
// check the cache first
teamID, ok := channelToTeamCache[boards[i].ChannelID]
// query DB if entry not found in cache
if !ok {
teamID, err = s.getBestTeamForBoard(s.db, boards[i])
if err != nil {
// don't let one board's error spoil
// the mood for others
s.logger.Error("could not find the best team for board during team less boards migration. Continuing", mlog.String("boardID", boards[i].ID))
continue
}
}
channelToTeamCache[boards[i].ChannelID] = teamID
boards[i].TeamID = teamID
query := s.getQueryBuilder(tx).
Update(s.tablePrefix+"boards").
Set("team_id", teamID).
Set("type", model.BoardTypePrivate).
Where(sq.Eq{"id": boards[i].ID})
if _, err := query.Exec(); err != nil {
s.logger.Error("failed to set team id for board", mlog.String("board_id", boards[i].ID), mlog.String("team_id", teamID), mlog.Err(err))
return err
}
}
if err := s.setSystemSetting(tx, TeamLessBoardsMigrationKey, strconv.FormatBool(true)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "runTeamLessBoardsMigration"))
}
return fmt.Errorf("cannot mark migration as completed: %w", err)
}
if err := tx.Commit(); err != nil {
s.logger.Error("failed to commit runTeamLessBoardsMigration transaction", mlog.Err(err))
return err
}
return nil
}
func (s *SQLStore) getDMBoards(tx sq.BaseRunner) ([]*model.Board, error) {
conditions := sq.And{
sq.Eq{"team_id": ""},
sq.Or{
sq.Eq{"type": "D"},
sq.Eq{"type": "G"},
},
}
boards, err := s.getLegacyBoardsByCondition(tx, conditions)
if err != nil && model.IsErrNotFound(err) {
return []*model.Board{}, nil
}
return boards, err
}
// The destination is selected as the first team where all members
// of the DM are a part of. If no such team exists,
// we use the first team to which DM creator belongs to.
func (s *SQLStore) getBestTeamForBoard(tx sq.BaseRunner, board *model.Board) (string, error) {
userTeams, err := s.getBoardUserTeams(tx, board)
if err != nil {
return "", err
}
teams := [][]interface{}{}
for _, userTeam := range userTeams {
userTeamInterfaces := make([]interface{}, len(userTeam))
for i := range userTeam {
userTeamInterfaces[i] = userTeam[i]
}
teams = append(teams, userTeamInterfaces)
}
commonTeams := utils.Intersection(teams...)
var teamID string
if len(commonTeams) > 0 {
teamID = commonTeams[0].(string)
} else {
// no common teams found. Let's try finding the best suitable team
if board.Type == "D" {
// get DM's creator and pick one of their team
channel, err := (s.servicesAPI).GetChannelByID(board.ChannelID)
if err != nil {
s.logger.Error("failed to fetch DM channel for board",
mlog.String("board_id", board.ID),
mlog.String("channel_id", board.ChannelID),
mlog.Err(err),
)
return "", err
}
if _, ok := userTeams[channel.CreatorId]; !ok {
s.logger.Error("channel creator not found in user teams",
mlog.String("board_id", board.ID),
mlog.String("channel_id", board.ChannelID),
mlog.String("creator_id", channel.CreatorId),
)
err := fmt.Errorf("%w board_id: %s, channel_id: %s, creator_id: %s", errChannelCreatorNotInTeam, board.ID, board.ChannelID, channel.CreatorId)
return "", err
}
teamID = userTeams[channel.CreatorId][0]
} else if board.Type == "G" {
// pick the team that has the most users as members
teamFrequency := map[string]int{}
highestFrequencyTeam := ""
highestFrequencyTeamFrequency := -1
for _, teams := range userTeams {
for _, teamID := range teams {
teamFrequency[teamID]++
if teamFrequency[teamID] > highestFrequencyTeamFrequency {
highestFrequencyTeamFrequency = teamFrequency[teamID]
highestFrequencyTeam = teamID
}
}
}
teamID = highestFrequencyTeam
}
}
return teamID, nil
}
func (s *SQLStore) getBoardUserTeams(tx sq.BaseRunner, board *model.Board) (map[string][]string, error) {
query := s.getQueryBuilder(tx).
Select("tm.UserId", "tm.TeamId").
From("ChannelMembers cm").
Join("TeamMembers tm ON cm.UserId = tm.UserId").
Join("Teams t ON tm.TeamId = t.Id").
Where(sq.Eq{
"cm.ChannelId": board.ChannelID,
"t.DeleteAt": 0,
"tm.DeleteAt": 0,
})
rows, err := query.Query()
if err != nil {
s.logger.Error("failed to fetch user teams for board", mlog.String("boardID", board.ID), mlog.String("channelID", board.ChannelID), mlog.Err(err))
return nil, err
}
defer rows.Close()
userTeams := map[string][]string{}
for rows.Next() {
var userID, teamID string
err := rows.Scan(&userID, &teamID)
if err != nil {
s.logger.Error("getBoardUserTeams failed to scan SQL query result", mlog.String("boardID", board.ID), mlog.String("channelID", board.ChannelID), mlog.Err(err))
return nil, err
}
userTeams[userID] = append(userTeams[userID], teamID)
}
return userTeams, nil
}
func (s *SQLStore) RunDeletedMembershipBoardsMigration() error {
if !s.isPlugin {
return nil
}
setting, err := s.GetSystemSetting(DeletedMembershipBoardsMigrationKey)
if err != nil {
return fmt.Errorf("cannot get deleted membership boards migration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
boards, err := s.getDeletedMembershipBoards(s.db)
if err != nil {
return err
}
if len(boards) == 0 {
s.logger.Debug("No boards with owner not anymore on their team found, marking runDeletedMembershipBoardsMigration as done")
if sErr := s.SetSystemSetting(DeletedMembershipBoardsMigrationKey, strconv.FormatBool(true)); sErr != nil {
return fmt.Errorf("cannot mark migration as completed: %w", sErr)
}
return nil
}
s.logger.Debug("Migrating boards with owner not anymore on their team", mlog.Int("count", len(boards)))
tx, err := s.db.BeginTx(context.Background(), nil)
if err != nil {
s.logger.Error("error starting transaction in runDeletedMembershipBoardsMigration", mlog.Err(err))
return err
}
for i := range boards {
teamID, err := s.getBestTeamForBoard(s.db, boards[i])
if err != nil {
// don't let one board's error spoil
// the mood for others
s.logger.Error("could not find the best team for board during deleted membership boards migration. Continuing", mlog.String("boardID", boards[i].ID))
continue
}
boards[i].TeamID = teamID
query := s.getQueryBuilder(tx).
Update(s.tablePrefix+"boards").
Set("team_id", teamID).
Where(sq.Eq{"id": boards[i].ID})
if _, err := query.Exec(); err != nil {
s.logger.Error("failed to set team id for board", mlog.String("board_id", boards[i].ID), mlog.String("team_id", teamID), mlog.Err(err))
return err
}
}
if err := s.setSystemSetting(tx, DeletedMembershipBoardsMigrationKey, strconv.FormatBool(true)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "runDeletedMembershipBoardsMigration"))
}
return fmt.Errorf("cannot mark migration as completed: %w", err)
}
if err := tx.Commit(); err != nil {
s.logger.Error("failed to commit runDeletedMembershipBoardsMigration transaction", mlog.Err(err))
return err
}
return nil
}
// getDeletedMembershipBoards retrieves those boards whose creator is
// associated to the board's team with a deleted team membership.
func (s *SQLStore) getDeletedMembershipBoards(tx sq.BaseRunner) ([]*model.Board, error) {
rows, err := s.getQueryBuilder(tx).
Select(legacyBoardFields("b.")...).
From(s.tablePrefix + "boards b").
Join("TeamMembers tm ON b.created_by = tm.UserId").
Where("b.team_id = tm.TeamId").
Where(sq.NotEq{"tm.DeleteAt": 0}).
Query()
if err != nil {
return nil, err
}
defer s.CloseRows(rows)
boards, err := s.boardsFromRows(rows)
if err != nil {
return nil, err
}
return boards, err
}
func (s *SQLStore) RunFixCollationsAndCharsetsMigration() error {
// This is for MySQL only
if s.dbType != model.MysqlDBType {
return nil
}
// get collation and charSet setting that Channels is using.
// when personal server or unit testing, no channels tables exist so just set to a default.
var collation string
var charSet string
var err error
if !s.isPlugin || os.Getenv("FOCALBOARD_UNIT_TESTING") == "1" {
collation = "utf8mb4_general_ci"
charSet = "utf8mb4"
} else {
collation, charSet, err = s.getCollationAndCharset("Channels")
if err != nil {
return err
}
}
// get all FocalBoard tables
tableNames, err := s.getFocalBoardTableNames()
if err != nil {
return err
}
merr := merror.New()
// alter each table if there is a collation or charset mismatch
for _, name := range tableNames {
tableCollation, tableCharSet, err := s.getCollationAndCharset(name)
if err != nil {
return err
}
if collation == tableCollation && charSet == tableCharSet {
// nothing to do
continue
}
s.logger.Warn(
"found collation/charset mismatch, fixing table",
mlog.String("tableName", name),
mlog.String("tableCollation", tableCollation),
mlog.String("tableCharSet", tableCharSet),
mlog.String("collation", collation),
mlog.String("charSet", charSet),
)
sql := fmt.Sprintf("ALTER TABLE %s CONVERT TO CHARACTER SET '%s' COLLATE '%s'", name, charSet, collation)
result, err := s.db.Exec(sql)
if err != nil {
merr.Append(err)
continue
}
num, err := result.RowsAffected()
if err != nil {
merr.Append(err)
}
if num > 0 {
s.logger.Debug("table collation and/or charSet fixed",
mlog.String("table_name", name),
)
}
}
return merr.ErrorOrNil()
}
func (s *SQLStore) getFocalBoardTableNames() ([]string, error) {
if s.dbType != model.MysqlDBType {
return nil, newErrInvalidDBType("getFocalBoardTableNames requires MySQL")
}
query := s.getQueryBuilder(s.db).
Select("table_name").
From("information_schema.tables").
Where(sq.Like{"table_name": s.tablePrefix + "%"}).
Where("table_schema=(SELECT DATABASE())")
rows, err := query.Query()
if err != nil {
return nil, fmt.Errorf("error fetching FocalBoard table names: %w", err)
}
defer rows.Close()
names := make([]string, 0)
for rows.Next() {
var tableName string
err := rows.Scan(&tableName)
if err != nil {
return nil, fmt.Errorf("cannot scan result while fetching table names: %w", err)
}
names = append(names, tableName)
}
return names, nil
}
func (s *SQLStore) getCollationAndCharset(tableName string) (string, string, error) {
if s.dbType != model.MysqlDBType {
return "", "", newErrInvalidDBType("getCollationAndCharset requires MySQL")
}
query := s.getQueryBuilder(s.db).
Select("table_collation").
From("information_schema.tables").
Where(sq.Eq{"table_name": tableName}).
Where("table_schema=(SELECT DATABASE())")
row := query.QueryRow()
var collation string
err := row.Scan(&collation)
if err != nil {
return "", "", fmt.Errorf("error fetching collation for table %s: %w", tableName, err)
}
// obtains the charset from the first column that has it set
query = s.getQueryBuilder(s.db).
Select("CHARACTER_SET_NAME").
From("information_schema.columns").
Where(sq.Eq{
"table_name": tableName,
}).
Where("table_schema=(SELECT DATABASE())").
Where(sq.NotEq{"CHARACTER_SET_NAME": "NULL"}).
Limit(1)
row = query.QueryRow()
var charSet string
err = row.Scan(&charSet)
if err != nil {
return "", "", fmt.Errorf("error fetching charSet: %w", err)
}
return collation, charSet, nil
}
func (s *SQLStore) RunDeDuplicateCategoryBoardsMigration(currentMigration int) error {
setting, err := s.GetSystemSetting(DeDuplicateCategoryBoardTableMigrationKey)
if err != nil {
return fmt.Errorf("cannot get DeDuplicateCategoryBoardTableMigration state: %w", err)
}
// If the migration is already completed, do not run it again.
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
return nil
}
if currentMigration >= (deDuplicateCategoryBoards + 1) {
// if the migration for which we're fixing the data is already applied,
// no need to check fix anything
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
return nil
}
needed, err := s.doesDuplicateCategoryBoardsExist()
if err != nil {
return err
}
if !needed {
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
}
if s.dbType == model.MysqlDBType {
return s.runMySQLDeDuplicateCategoryBoardsMigration()
} else if s.dbType == model.PostgresDBType {
return s.runPostgresDeDuplicateCategoryBoardsMigration()
}
if mErr := s.setSystemSetting(s.db, DeDuplicateCategoryBoardTableMigrationKey, strconv.FormatBool(true)); mErr != nil {
return fmt.Errorf("cannot mark migration %s as completed: %w", "RunDeDuplicateCategoryBoardsMigration", mErr)
}
return nil
}
func (s *SQLStore) doesDuplicateCategoryBoardsExist() (bool, error) {
subQuery := s.getQueryBuilder(s.db).
Select("user_id", "board_id", "count(*) AS count").
From(s.tablePrefix+"category_boards").
GroupBy("user_id", "board_id").
Having("count(*) > 1")
query := s.getQueryBuilder(s.db).
Select("COUNT(user_id)").
FromSelect(subQuery, "duplicate_dataset")
row := query.QueryRow()
count := 0
if err := row.Scan(&count); err != nil {
s.logger.Error("Error occurred reading number of duplicate records in category_boards table", mlog.Err(err))
return false, err
}
return count > 0, nil
}
func (s *SQLStore) runMySQLDeDuplicateCategoryBoardsMigration() error {
query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " +
"FROM " + s.tablePrefix + "category_boards) " +
"DELETE " + s.tablePrefix + "category_boards FROM " + s.tablePrefix + "category_boards " +
"JOIN duplicates USING(id) WHERE duplicates.rownum > 1;"
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err))
}
return nil
}
func (s *SQLStore) runPostgresDeDuplicateCategoryBoardsMigration() error {
query := "WITH duplicates AS (SELECT id, ROW_NUMBER() OVER(PARTITION BY user_id, board_id) AS rownum " +
"FROM " + s.tablePrefix + "category_boards) " +
"DELETE FROM " + s.tablePrefix + "category_boards USING duplicates " +
"WHERE " + s.tablePrefix + "category_boards.id = duplicates.id AND duplicates.rownum > 1;"
if _, err := s.db.Exec(query); err != nil {
s.logger.Error("Failed to de-duplicate data in category_boards table", mlog.Err(err))
}
return nil
}

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

@@ -0,0 +1,265 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetBlocksWithSameID(t *testing.T) {
t.Skip("we need to setup a test with the database migrated up to version 14 and then run these tests")
RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) {
container1 := "1"
container2 := "2"
container3 := "3"
block1 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"}
block2 := &model.Block{ID: "block-id-2", BoardID: "board-id-2"}
block3 := &model.Block{ID: "block-id-3", BoardID: "board-id-3"}
block4 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"}
block5 := &model.Block{ID: "block-id-2", BoardID: "board-id-2"}
block6 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"}
block7 := &model.Block{ID: "block-id-7", BoardID: "board-id-7"}
block8 := &model.Block{ID: "block-id-8", BoardID: "board-id-8"}
for _, block := range []*model.Block{block1, block2, block3} {
err := sqlStore.insertLegacyBlock(sqlStore.db, container1, block, "user-id")
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
}
for _, block := range []*model.Block{block4, block5} {
err := sqlStore.insertLegacyBlock(sqlStore.db, container2, block, "user-id")
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
}
for _, block := range []*model.Block{block6, block7, block8} {
err := sqlStore.insertLegacyBlock(sqlStore.db, container3, block, "user-id")
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
}
blocksWithDuplicatedID := []*model.Block{block1, block2, block4, block5, block6}
blocks, err := sqlStore.getBlocksWithSameID(sqlStore.db)
require.NoError(t, err)
// we process the found blocks to remove extra information and be
// able to compare both expected and found sets
foundBlocks := []*model.Block{}
for _, foundBlock := range blocks {
foundBlocks = append(foundBlocks, &model.Block{ID: foundBlock.ID, BoardID: foundBlock.BoardID})
}
require.ElementsMatch(t, blocksWithDuplicatedID, foundBlocks)
})
}
func TestReplaceBlockID(t *testing.T) {
t.Skip("we need to setup a test with the database migrated up to version 14 and then run these tests")
RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) {
container1 := "1"
container2 := "2"
// blocks from team1
block1 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"}
block2 := &model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"}
block3 := &model.Block{ID: "block-id-3", BoardID: "block-id-1"}
block4 := &model.Block{ID: "block-id-4", BoardID: "block-id-2"}
block5 := &model.Block{ID: "block-id-5", BoardID: "block-id-1", ParentID: "block-id-1"}
block8 := &model.Block{
ID: "block-id-8", BoardID: "board-id-2", Type: model.TypeCard,
Fields: map[string]interface{}{"contentOrder": []string{"block-id-1", "block-id-2"}},
}
// blocks from team2. They're identical to blocks 1 and 2,
// but they shouldn't change
block6 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"}
block7 := &model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"}
block9 := &model.Block{
ID: "block-id-8", BoardID: "board-id-2", Type: model.TypeCard,
Fields: map[string]interface{}{"contentOrder": []string{"block-id-1", "block-id-2"}},
}
for _, block := range []*model.Block{block1, block2, block3, block4, block5, block8} {
err := sqlStore.insertLegacyBlock(sqlStore.db, container1, block, "user-id")
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
}
for _, block := range []*model.Block{block6, block7, block9} {
err := sqlStore.insertLegacyBlock(sqlStore.db, container2, block, "user-id")
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
}
currentID := "block-id-1"
newID := "new-id-1"
err := sqlStore.replaceBlockID(sqlStore.db, currentID, newID, "1")
require.NoError(t, err)
newBlock1, err := sqlStore.getLegacyBlock(sqlStore.db, container1, newID)
require.NoError(t, err)
newBlock2, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block2.ID)
require.NoError(t, err)
newBlock3, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block3.ID)
require.NoError(t, err)
newBlock5, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block5.ID)
require.NoError(t, err)
newBlock6, err := sqlStore.getLegacyBlock(sqlStore.db, container2, block6.ID)
require.NoError(t, err)
newBlock7, err := sqlStore.getLegacyBlock(sqlStore.db, container2, block7.ID)
require.NoError(t, err)
newBlock8, err := sqlStore.GetBlock(block8.ID)
require.NoError(t, err)
newBlock9, err := sqlStore.GetBlock(block9.ID)
require.NoError(t, err)
require.Equal(t, newID, newBlock1.ID)
require.Equal(t, newID, newBlock2.ParentID)
require.Equal(t, newID, newBlock3.BoardID)
require.Equal(t, newID, newBlock5.BoardID)
require.Equal(t, newID, newBlock5.ParentID)
require.Equal(t, newBlock8.Fields["contentOrder"].([]interface{})[0], newID)
require.Equal(t, newBlock8.Fields["contentOrder"].([]interface{})[1], "block-id-2")
require.Equal(t, currentID, newBlock6.ID)
require.Equal(t, currentID, newBlock7.ParentID)
require.Equal(t, newBlock9.Fields["contentOrder"].([]interface{})[0], "block-id-1")
require.Equal(t, newBlock9.Fields["contentOrder"].([]interface{})[1], "block-id-2")
})
}
func TestRunUniqueIDsMigration(t *testing.T) {
t.Skip("we need to setup a test with the database migrated up to version 14 and then run these tests")
RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) {
// we need to mark the migration as not done so we can run it
// again with the test data
keyErr := sqlStore.SetSystemSetting(UniqueIDsMigrationKey, "false")
require.NoError(t, keyErr)
container1 := "1"
container2 := "2"
container3 := "3"
// blocks from workspace1. They shouldn't change, as the first
// duplicated ID is preserved
block1 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"}
block2 := &model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"}
block3 := &model.Block{ID: "block-id-3", BoardID: "block-id-1"}
// blocks from workspace2. They're identical to blocks 1, 2 and 3,
// and they should change
block4 := &model.Block{ID: "block-id-1", BoardID: "board-id-1"}
block5 := &model.Block{ID: "block-id-2", BoardID: "board-id-2", ParentID: "block-id-1"}
block6 := &model.Block{ID: "block-id-6", BoardID: "block-id-1", ParentID: "block-id-2"}
// block from workspace3. It should change as well
block7 := &model.Block{ID: "block-id-2", BoardID: "board-id-2"}
for _, block := range []*model.Block{block1, block2, block3} {
err := sqlStore.insertLegacyBlock(sqlStore.db, container1, block, "user-id-2")
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
}
for _, block := range []*model.Block{block4, block5, block6} {
err := sqlStore.insertLegacyBlock(sqlStore.db, container2, block, "user-id-2")
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
}
for _, block := range []*model.Block{block7} {
err := sqlStore.insertLegacyBlock(sqlStore.db, container3, block, "user-id-2")
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
}
err := sqlStore.RunUniqueIDsMigration()
require.NoError(t, err)
// blocks from workspace 1 haven't changed, so we can simply fetch them
newBlock1, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block1.ID)
require.NoError(t, err)
require.NotNil(t, newBlock1)
newBlock2, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block2.ID)
require.NoError(t, err)
require.NotNil(t, newBlock2)
newBlock3, err := sqlStore.getLegacyBlock(sqlStore.db, container1, block3.ID)
require.NoError(t, err)
require.NotNil(t, newBlock3)
// first two blocks from workspace 2 have changed, so we fetch
// them through the third one, which points to the new IDs
newBlock6, err := sqlStore.getLegacyBlock(sqlStore.db, container2, block6.ID)
require.NoError(t, err)
require.NotNil(t, newBlock6)
newBlock4, err := sqlStore.getLegacyBlock(sqlStore.db, container2, newBlock6.BoardID)
require.NoError(t, err)
require.NotNil(t, newBlock4)
newBlock5, err := sqlStore.getLegacyBlock(sqlStore.db, container2, newBlock6.ParentID)
require.NoError(t, err)
require.NotNil(t, newBlock5)
// block from workspace 3 changed as well, so we shouldn't be able
// to fetch it
newBlock7, err := sqlStore.getLegacyBlock(sqlStore.db, container3, block7.ID)
require.NoError(t, err)
require.Nil(t, newBlock7)
// workspace 1 block links are maintained
require.Equal(t, newBlock1.ID, newBlock2.ParentID)
require.Equal(t, newBlock1.ID, newBlock3.BoardID)
// workspace 2 first two block IDs have changed
require.NotEqual(t, block4.ID, newBlock4.BoardID)
require.NotEqual(t, block5.ID, newBlock5.ParentID)
})
}
func TestCheckForMismatchedCollation(t *testing.T) {
RunStoreTestsWithSqlStore(t, func(t *testing.T, sqlStore *SQLStore) {
if sqlStore.dbType != model.MysqlDBType {
return
}
// make sure all collations are consistent.
tableNames, err := sqlStore.getFocalBoardTableNames()
require.NoError(t, err)
sqlCollation := "SELECT table_collation FROM information_schema.tables WHERE table_name=? and table_schema=(SELECT DATABASE())"
stmtCollation, err := sqlStore.db.Prepare(sqlCollation)
require.NoError(t, err)
defer stmtCollation.Close()
var collation string
// make sure the correct charset is applied to each table.
for i, name := range tableNames {
row := stmtCollation.QueryRow(name)
var actualCollation string
err = row.Scan(&actualCollation)
require.NoError(t, err)
if collation == "" {
collation = actualCollation
}
assert.Equalf(t, collation, actualCollation, "for table_name='%s', index=%d", name, i)
}
})
}

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

@@ -0,0 +1,179 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"strings"
"time"
"github.com/pkg/errors"
sq "github.com/Masterminds/squirrel"
_ "github.com/lib/pq" // postgres driver
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type RetentionTableDeletionInfo struct {
Table string
PrimaryKeys []string
BoardIDColumn string
}
func (s *SQLStore) runDataRetention(db sq.BaseRunner, globalRetentionDate int64, batchSize int64) (int64, error) {
s.logger.Info("Start Boards Data Retention",
mlog.String("Global Retention Date", time.Unix(globalRetentionDate/1000, 0).String()),
mlog.Int64("Raw Date", globalRetentionDate))
deleteTables := []RetentionTableDeletionInfo{
{
Table: "blocks",
PrimaryKeys: []string{"id"},
BoardIDColumn: "board_id",
},
{
Table: "blocks_history",
PrimaryKeys: []string{"id"},
BoardIDColumn: "board_id",
},
{
Table: "boards",
PrimaryKeys: []string{"id"},
BoardIDColumn: "id",
},
{
Table: "boards_history",
PrimaryKeys: []string{"id"},
BoardIDColumn: "id",
},
{
Table: "board_members",
PrimaryKeys: []string{"board_id"},
BoardIDColumn: "board_id",
},
{
Table: "board_members_history",
PrimaryKeys: []string{"board_id"},
BoardIDColumn: "board_id",
},
{
Table: "sharing",
PrimaryKeys: []string{"id"},
BoardIDColumn: "id",
},
{
Table: "category_boards",
PrimaryKeys: []string{"id"},
BoardIDColumn: "board_id",
},
}
subBuilder := s.getQueryBuilder(db).
Select("board_id, MAX(update_at) AS maxDate").
From(s.tablePrefix + "blocks").
GroupBy("board_id")
subQuery, _, _ := subBuilder.ToSql()
builder := s.getQueryBuilder(db).
Select("id").
From(s.tablePrefix + "boards").
LeftJoin("( " + subQuery + " ) As subquery ON (subquery.board_id = id)").
Where(sq.Lt{"maxDate": globalRetentionDate}).
Where(sq.NotEq{"team_id": "0"}).
Where(sq.Eq{"is_template": false})
rows, err := builder.Query()
if err != nil {
s.logger.Error(`dataRetention subquery ERROR`, mlog.Err(err))
return 0, err
}
defer s.CloseRows(rows)
deleteIds, err := idsFromRows(rows)
if err != nil {
return 0, err
}
totalAffected := 0
if len(deleteIds) > 0 {
for _, table := range deleteTables {
affected, err := s.genericRetentionPoliciesDeletion(db, table, deleteIds, batchSize)
if err != nil {
return int64(totalAffected), err
}
totalAffected += int(affected)
}
}
s.logger.Info("Complete Boards Data Retention",
mlog.Int("Total deletion ids", len(deleteIds)),
mlog.Int("TotalAffected", totalAffected))
return int64(totalAffected), nil
}
func idsFromRows(rows *sql.Rows) ([]string, error) {
deleteIds := []string{}
for rows.Next() {
var boardID string
err := rows.Scan(
&boardID,
)
if err != nil {
return nil, err
}
deleteIds = append(deleteIds, boardID)
}
return deleteIds, nil
}
// genericRetentionPoliciesDeletion actually executes the DELETE query
// using a sq.SelectBuilder which selects the rows to delete.
func (s *SQLStore) genericRetentionPoliciesDeletion(
db sq.BaseRunner,
info RetentionTableDeletionInfo,
deleteIds []string,
batchSize int64,
) (int64, error) {
whereClause := info.BoardIDColumn + " IN ('" + strings.Join(deleteIds, "','") + "')"
deleteQuery := s.getQueryBuilder(db).
Delete(s.tablePrefix + info.Table).
Where(whereClause)
if batchSize > 0 {
deleteQuery.Limit(uint64(batchSize))
primaryKeysStr := "(" + strings.Join(info.PrimaryKeys, ",") + ")"
if s.dbType != model.MysqlDBType {
selectQuery := s.getQueryBuilder(db).
Select(primaryKeysStr).
From(s.tablePrefix + info.Table).
Where(whereClause).
Limit(uint64(batchSize))
selectString, _, _ := selectQuery.ToSql()
deleteQuery = s.getQueryBuilder(db).
Delete(s.tablePrefix + info.Table).
Where(primaryKeysStr + " IN (" + selectString + ")")
}
}
var totalRowsAffected int64
var batchRowsAffected int64
for {
result, err := deleteQuery.Exec()
if err != nil {
return 0, errors.Wrap(err, "failed to delete "+info.Table)
}
batchRowsAffected, err = result.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "failed to get rows affected for "+info.Table)
}
totalRowsAffected += batchRowsAffected
if batchRowsAffected != batchSize {
break
}
}
return totalRowsAffected, nil
}

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

@@ -0,0 +1,95 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"errors"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *mm_model.FileInfo) error {
query := s.getQueryBuilder(db).
Insert(s.tablePrefix+"file_info").
Columns(
"id",
"create_at",
"name",
"extension",
"size",
"delete_at",
"path",
"archived",
).
Values(
fileInfo.Id,
fileInfo.CreateAt,
fileInfo.Name,
fileInfo.Extension,
fileInfo.Size,
fileInfo.DeleteAt,
fileInfo.Path,
false,
)
if _, err := query.Exec(); err != nil {
s.logger.Error(
"failed to save fileinfo",
mlog.String("file_name", fileInfo.Name),
mlog.Int64("size", fileInfo.Size),
mlog.Err(err),
)
return err
}
return nil
}
func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*mm_model.FileInfo, error) {
query := s.getQueryBuilder(db).
Select(
"id",
"create_at",
"delete_at",
"name",
"extension",
"size",
"archived",
"path",
).
From(s.tablePrefix + "file_info").
Where(sq.Eq{"Id": id})
row := query.QueryRow()
fileInfo := mm_model.FileInfo{}
err := row.Scan(
&fileInfo.Id,
&fileInfo.CreateAt,
&fileInfo.DeleteAt,
&fileInfo.Name,
&fileInfo.Extension,
&fileInfo.Size,
&fileInfo.Archived,
&fileInfo.Path,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, model.NewErrNotFound("file info ID=" + id)
}
s.logger.Error("error scanning fileinfo row", mlog.String("id", id), mlog.Err(err))
return nil, err
}
return &fileInfo, nil
}

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

@@ -0,0 +1,263 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"encoding/json"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
sq "github.com/Masterminds/squirrel"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func legacyBoardFields(prefix string) []string {
// substitute new columns with `"\"\""` (empty string) so as to allow
// row scan to continue to work with new models.
fields := []string{
"id",
"team_id",
"COALESCE(channel_id, '')",
"COALESCE(created_by, '')",
"modified_by",
"type",
"''", // substitute for minimum_role column.
"title",
"description",
"icon",
"show_description",
"is_template",
"template_version",
"COALESCE(properties, '{}')",
"COALESCE(card_properties, '[]')",
"create_at",
"update_at",
"delete_at",
}
if prefix == "" {
return fields
}
prefixedFields := make([]string, len(fields))
for i, field := range fields {
switch {
case strings.HasPrefix(field, "COALESCE("):
prefixedFields[i] = strings.Replace(field, "COALESCE(", "COALESCE("+prefix, 1)
case field == "''":
prefixedFields[i] = field
default:
prefixedFields[i] = prefix + field
}
}
return prefixedFields
}
// legacyBlocksFromRows is the old getBlock version that still uses
// the old block model. This method is kept to enable the unique IDs
// data migration.
//
//nolint:unused
func (s *SQLStore) legacyBlocksFromRows(rows *sql.Rows) ([]*model.Block, error) {
results := []*model.Block{}
for rows.Next() {
var block model.Block
var fieldsJSON string
var modifiedBy sql.NullString
var insertAt string
err := rows.Scan(
&block.ID,
&block.ParentID,
&block.BoardID,
&block.CreatedBy,
&modifiedBy,
&block.Schema,
&block.Type,
&block.Title,
&fieldsJSON,
&insertAt,
&block.CreateAt,
&block.UpdateAt,
&block.DeleteAt,
&block.WorkspaceID)
if err != nil {
// handle this error
s.logger.Error(`ERROR blocksFromRows`, mlog.Err(err))
return nil, err
}
if modifiedBy.Valid {
block.ModifiedBy = modifiedBy.String
}
err = json.Unmarshal([]byte(fieldsJSON), &block.Fields)
if err != nil {
// handle this error
s.logger.Error(`ERROR blocksFromRows fields`, mlog.Err(err))
return nil, err
}
results = append(results, &block)
}
return results, nil
}
// getLegacyBlock is the old getBlock version that still uses the old
// block model. This method is kept to enable the unique IDs data
// migration.
//
//nolint:unused
func (s *SQLStore) getLegacyBlock(db sq.BaseRunner, workspaceID string, blockID string) (*model.Block, error) {
query := s.getQueryBuilder(db).
Select(
"id",
"parent_id",
"root_id",
"created_by",
"modified_by",
s.escapeField("schema"),
"type",
"title",
"COALESCE(fields, '{}')",
"insert_at",
"create_at",
"update_at",
"delete_at",
"COALESCE(workspace_id, '0')",
).
From(s.tablePrefix + "blocks").
Where(sq.Eq{"id": blockID}).
Where(sq.Eq{"coalesce(workspace_id, '0')": workspaceID})
rows, err := query.Query()
if err != nil {
s.logger.Error(`GetBlock ERROR`, mlog.Err(err))
return nil, err
}
blocks, err := s.legacyBlocksFromRows(rows)
if err != nil {
return nil, err
}
if len(blocks) == 0 {
return nil, nil
}
return blocks[0], nil
}
// insertLegacyBlock is the old insertBlock version that still uses
// the old block model. This method is kept to enable the unique IDs
// data migration.
//
//nolint:unused
func (s *SQLStore) insertLegacyBlock(db sq.BaseRunner, workspaceID string, block *model.Block, userID string) error {
if block.BoardID == "" {
return ErrEmptyBoardID{}
}
fieldsJSON, err := json.Marshal(block.Fields)
if err != nil {
return err
}
existingBlock, err := s.getLegacyBlock(db, workspaceID, block.ID)
if err != nil {
return err
}
block.UpdateAt = utils.GetMillis()
block.ModifiedBy = userID
insertQuery := s.getQueryBuilder(db).Insert("").
Columns(
"workspace_id",
"id",
"parent_id",
"root_id",
"created_by",
"modified_by",
s.escapeField("schema"),
"type",
"title",
"fields",
"create_at",
"update_at",
"delete_at",
)
insertQueryValues := map[string]interface{}{
"workspace_id": workspaceID,
"id": block.ID,
"parent_id": block.ParentID,
"root_id": block.BoardID,
s.escapeField("schema"): block.Schema,
"type": block.Type,
"title": block.Title,
"fields": fieldsJSON,
"delete_at": block.DeleteAt,
"created_by": block.CreatedBy,
"modified_by": block.ModifiedBy,
"create_at": block.CreateAt,
"update_at": block.UpdateAt,
}
if existingBlock != nil {
// block with ID exists, so this is an update operation
query := s.getQueryBuilder(db).Update(s.tablePrefix+"blocks").
Where(sq.Eq{"id": block.ID}).
Where(sq.Eq{"COALESCE(workspace_id, '0')": workspaceID}).
Set("parent_id", block.ParentID).
Set("root_id", block.BoardID).
Set("modified_by", block.ModifiedBy).
Set(s.escapeField("schema"), block.Schema).
Set("type", block.Type).
Set("title", block.Title).
Set("fields", fieldsJSON).
Set("update_at", block.UpdateAt).
Set("delete_at", block.DeleteAt)
if _, err := query.Exec(); err != nil {
s.logger.Error(`InsertBlock error occurred while updating existing block`, mlog.String("blockID", block.ID), mlog.Err(err))
return err
}
} else {
block.CreatedBy = userID
block.CreateAt = utils.GetMillis()
insertQueryValues["created_by"] = block.CreatedBy
insertQueryValues["create_at"] = block.CreateAt
insertQueryValues["update_at"] = block.UpdateAt
insertQueryValues["modified_by"] = block.ModifiedBy
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks")
if _, err := query.Exec(); err != nil {
return err
}
}
// writing block history
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "blocks_history")
if _, err := query.Exec(); err != nil {
return err
}
return nil
}
func (s *SQLStore) getLegacyBoardsByCondition(db sq.BaseRunner, conditions ...interface{}) ([]*model.Board, error) {
return s.getBoardsFieldsByCondition(db, legacyBoardFields(""), conditions...)
}

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

@@ -0,0 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
mainStoreTypes = initStores(false)
status := m.Run()
for _, st := range mainStoreTypes {
_ = st.Store.Shutdown()
_ = st.Logger.Shutdown()
}
os.Exit(status)
}

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

@@ -0,0 +1,655 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"bytes"
"context"
"database/sql"
"embed"
"errors"
"fmt"
"strings"
"text/template"
sq "github.com/Masterminds/squirrel"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/sqlstore"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/morph"
drivers "github.com/mattermost/morph/drivers"
mysql "github.com/mattermost/morph/drivers/mysql"
postgres "github.com/mattermost/morph/drivers/postgres"
embedded "github.com/mattermost/morph/sources/embedded"
_ "github.com/lib/pq" // postgres driver
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
//go:embed migrations/*.sql
var Assets embed.FS
const (
uniqueIDsMigrationRequiredVersion = 14
teamLessBoardsMigrationRequiredVersion = 18
categoriesUUIDIDMigrationRequiredVersion = 20
deDuplicateCategoryBoards = 35
tempSchemaMigrationTableName = "temp_schema_migration"
)
var errChannelCreatorNotInTeam = errors.New("channel creator not found in user teams")
// migrations in MySQL need to run with the multiStatements flag
// enabled, so this method creates a new connection ensuring that it's
// enabled.
func (s *SQLStore) getMigrationConnection() (*sql.DB, error) {
connectionString := s.connectionString
if s.dbType == model.MysqlDBType {
var err error
connectionString, err = sqlstore.ResetReadTimeout(connectionString)
if err != nil {
return nil, err
}
connectionString, err = sqlstore.AppendMultipleStatementsFlag(connectionString)
if err != nil {
return nil, err
}
}
var settings mm_model.SqlSettings
settings.SetDefaults(false)
if s.configFn != nil {
settings = s.configFn().SqlSettings
}
*settings.DriverName = s.dbType
db := sqlstore.SetupConnection("master", connectionString, &settings)
return db, nil
}
func (s *SQLStore) Migrate() error {
if err := s.EnsureSchemaMigrationFormat(); err != nil {
return err
}
defer func() {
// the old schema migration table deletion happens after the
// migrations have run, to be able to recover its information
// in case there would be errors during the process.
if err := s.deleteOldSchemaMigrationTable(); err != nil {
s.logger.Error("cannot delete the old schema migration table", mlog.Err(err))
}
}()
var driver drivers.Driver
var err error
var db *sql.DB
s.logger.Debug("Getting migrations connection")
db, err = s.getMigrationConnection()
if err != nil {
return err
}
defer func() {
s.logger.Debug("Closing migrations connection")
db.Close()
}()
if s.dbType == model.PostgresDBType {
driver, err = postgres.WithInstance(db)
if err != nil {
return err
}
}
if s.dbType == model.MysqlDBType {
driver, err = mysql.WithInstance(db)
if err != nil {
return err
}
}
assetsList, err := Assets.ReadDir("migrations")
if err != nil {
return err
}
assetNamesForDriver := make([]string, len(assetsList))
for i, dirEntry := range assetsList {
assetNamesForDriver[i] = dirEntry.Name()
}
params := map[string]interface{}{
"prefix": s.tablePrefix,
"postgres": s.dbType == model.PostgresDBType,
"mysql": s.dbType == model.MysqlDBType,
"plugin": s.isPlugin,
"singleUser": s.isSingleUser,
}
migrationAssets := &embedded.AssetSource{
Names: assetNamesForDriver,
AssetFunc: func(name string) ([]byte, error) {
asset, mErr := Assets.ReadFile("migrations/" + name)
if mErr != nil {
return nil, mErr
}
tmpl, pErr := template.New("sql").Funcs(s.GetTemplateHelperFuncs()).Parse(string(asset))
if pErr != nil {
return nil, pErr
}
buffer := bytes.NewBufferString("")
err = tmpl.Execute(buffer, params)
if err != nil {
return nil, err
}
s.logger.Trace("migration template",
mlog.String("name", name),
mlog.String("sql", buffer.String()),
)
return buffer.Bytes(), nil
},
}
src, err := embedded.WithInstance(migrationAssets)
if err != nil {
return err
}
opts := []morph.EngineOption{
morph.WithLock("boards-lock-key"),
morph.SetMigrationTableName(fmt.Sprintf("%sschema_migrations", s.tablePrefix)),
morph.SetStatementTimeoutInSeconds(1000000),
}
s.logger.Debug("Creating migration engine")
engine, err := morph.New(context.Background(), driver, src, opts...)
if err != nil {
return err
}
defer func() {
s.logger.Debug("Closing migration engine")
engine.Close()
}()
return s.runMigrationSequence(engine, driver)
}
// runMigrationSequence executes all the migrations in order, both
// plain SQL and data migrations.
func (s *SQLStore) runMigrationSequence(engine *morph.Morph, driver drivers.Driver) error {
if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, uniqueIDsMigrationRequiredVersion); mErr != nil {
return mErr
}
if mErr := s.RunUniqueIDsMigration(); mErr != nil {
return fmt.Errorf("error running unique IDs migration: %w", mErr)
}
if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, teamLessBoardsMigrationRequiredVersion); mErr != nil {
return mErr
}
if mErr := s.RunTeamLessBoardsMigration(); mErr != nil {
return fmt.Errorf("error running teamless boards migration: %w", mErr)
}
if mErr := s.RunDeletedMembershipBoardsMigration(); mErr != nil {
return fmt.Errorf("error running deleted membership boards migration: %w", mErr)
}
if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, categoriesUUIDIDMigrationRequiredVersion); mErr != nil {
return mErr
}
if mErr := s.RunCategoryUUIDIDMigration(); mErr != nil {
return fmt.Errorf("error running categoryID migration: %w", mErr)
}
appliedMigrations, err := driver.AppliedMigrations()
if err != nil {
return err
}
if mErr := s.ensureMigrationsAppliedUpToVersion(engine, driver, deDuplicateCategoryBoards); mErr != nil {
return mErr
}
currentMigrationVersion := len(appliedMigrations)
if mErr := s.RunDeDuplicateCategoryBoardsMigration(currentMigrationVersion); mErr != nil {
return mErr
}
s.logger.Debug("== Applying all remaining migrations ====================",
mlog.Int("current_version", len(appliedMigrations)),
)
if err := engine.ApplyAll(); err != nil {
return err
}
// always run the collations & charset fix-ups
if mErr := s.RunFixCollationsAndCharsetsMigration(); mErr != nil {
return fmt.Errorf("error running fix collations and charsets migration: %w", mErr)
}
return nil
}
func (s *SQLStore) ensureMigrationsAppliedUpToVersion(engine *morph.Morph, driver drivers.Driver, version int) error {
applied, err := driver.AppliedMigrations()
if err != nil {
return err
}
currentVersion := len(applied)
s.logger.Debug("== Ensuring migrations applied up to version ====================",
mlog.Int("version", version),
mlog.Int("current_version", currentVersion))
// if the target version is below or equal to the current one, do
// not migrate either because is not needed (both are equal) or
// because it would downgrade the database (is below)
if version <= currentVersion {
s.logger.Debug("-- There is no need of applying any migration --------------------")
return nil
}
for _, migration := range applied {
s.logger.Debug("-- Found applied migration --------------------", mlog.Uint32("version", migration.Version), mlog.String("name", migration.Name))
}
if _, err = engine.Apply(version - currentVersion); err != nil {
return err
}
return nil
}
func (s *SQLStore) GetTemplateHelperFuncs() template.FuncMap {
funcs := template.FuncMap{
"addColumnIfNeeded": s.genAddColumnIfNeeded,
"dropColumnIfNeeded": s.genDropColumnIfNeeded,
"createIndexIfNeeded": s.genCreateIndexIfNeeded,
"renameTableIfNeeded": s.genRenameTableIfNeeded,
"renameColumnIfNeeded": s.genRenameColumnIfNeeded,
"doesTableExist": s.doesTableExist,
"doesColumnExist": s.doesColumnExist,
"addConstraintIfNeeded": s.genAddConstraintIfNeeded,
}
return funcs
}
func (s *SQLStore) genAddColumnIfNeeded(tableName, columnName, datatype, constraint string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
switch s.dbType {
case model.MysqlDBType:
vars := map[string]string{
"schema": s.schemaName,
"table_name": tableName,
"norm_table_name": normTableName,
"column_name": columnName,
"data_type": datatype,
"constraint": constraint,
}
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND column_name = '[[column_name]]'
) > 0,
'SELECT 1;',
'ALTER TABLE [[norm_table_name]] ADD COLUMN [[column_name]] [[data_type]] [[constraint]];'
));
PREPARE addColumnIfNeeded FROM @stmt;
EXECUTE addColumnIfNeeded;
DEALLOCATE PREPARE addColumnIfNeeded;
`, vars), nil
case model.PostgresDBType:
return fmt.Sprintf("\nALTER TABLE %s ADD COLUMN IF NOT EXISTS %s %s %s;\n", normTableName, columnName, datatype, constraint), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) genDropColumnIfNeeded(tableName, columnName string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
switch s.dbType {
case model.MysqlDBType:
vars := map[string]string{
"schema": s.schemaName,
"table_name": tableName,
"norm_table_name": normTableName,
"column_name": columnName,
}
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND column_name = '[[column_name]]'
) > 0,
'ALTER TABLE [[norm_table_name]] DROP COLUMN [[column_name]];',
'SELECT 1;'
));
PREPARE dropColumnIfNeeded FROM @stmt;
EXECUTE dropColumnIfNeeded;
DEALLOCATE PREPARE dropColumnIfNeeded;
`, vars), nil
case model.PostgresDBType:
return fmt.Sprintf("\nALTER TABLE %s DROP COLUMN IF EXISTS %s;\n", normTableName, columnName), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) genCreateIndexIfNeeded(tableName, columns string) (string, error) {
indexName := getIndexName(tableName, columns)
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
switch s.dbType {
case model.MysqlDBType:
vars := map[string]string{
"schema": s.schemaName,
"table_name": tableName,
"norm_table_name": normTableName,
"index_name": indexName,
"columns": columns,
}
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(index_name) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND index_name = '[[index_name]]'
) > 0,
'SELECT 1;',
'CREATE INDEX [[index_name]] ON [[norm_table_name]] ([[columns]]);'
));
PREPARE createIndexIfNeeded FROM @stmt;
EXECUTE createIndexIfNeeded;
DEALLOCATE PREPARE createIndexIfNeeded;
`, vars), nil
case model.PostgresDBType:
return fmt.Sprintf("\nCREATE INDEX IF NOT EXISTS %s ON %s (%s);\n", indexName, normTableName, columns), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) genRenameTableIfNeeded(oldTableName, newTableName string) (string, error) {
oldTableName = addPrefixIfNeeded(oldTableName, s.tablePrefix)
newTableName = addPrefixIfNeeded(newTableName, s.tablePrefix)
normOldTableName := normalizeTablename(s.schemaName, oldTableName)
vars := map[string]string{
"schema": s.schemaName,
"table_name": newTableName,
"norm_old_table_name": normOldTableName,
"new_table_name": newTableName,
}
switch s.dbType {
case model.MysqlDBType:
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
) > 0,
'SELECT 1;',
'RENAME TABLE [[norm_old_table_name]] TO [[new_table_name]];'
));
PREPARE renameTableIfNeeded FROM @stmt;
EXECUTE renameTableIfNeeded;
DEALLOCATE PREPARE renameTableIfNeeded;
`, vars), nil
case model.PostgresDBType:
return replaceVars(`
do $$
begin
if (SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES
WHERE table_name = '[[new_table_name]]'
AND table_schema = '[[schema]]'
) = 0 then
ALTER TABLE [[norm_old_table_name]] RENAME TO [[new_table_name]];
end if;
end$$;
`, vars), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) genRenameColumnIfNeeded(tableName, oldColumnName, newColumnName, dataType string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
vars := map[string]string{
"schema": s.schemaName,
"table_name": tableName,
"norm_table_name": normTableName,
"old_column_name": oldColumnName,
"new_column_name": newColumnName,
"data_type": dataType,
}
switch s.dbType {
case model.MysqlDBType:
return replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND column_name = '[[new_column_name]]'
) > 0,
'SELECT 1;',
'ALTER TABLE [[norm_table_name]] CHANGE [[old_column_name]] [[new_column_name]] [[data_type]];'
));
PREPARE renameColumnIfNeeded FROM @stmt;
EXECUTE renameColumnIfNeeded;
DEALLOCATE PREPARE renameColumnIfNeeded;
`, vars), nil
case model.PostgresDBType:
return replaceVars(`
do $$
begin
if (SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = '[[table_name]]'
AND table_schema = '[[schema]]'
AND column_name = '[[new_column_name]]'
) = 0 then
ALTER TABLE [[norm_table_name]] RENAME COLUMN [[old_column_name]] TO [[new_column_name]];
end if;
end$$;
`, vars), nil
default:
return "", ErrUnsupportedDatabaseType
}
}
func (s *SQLStore) doesTableExist(tableName string) (bool, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
query := s.getQueryBuilder(s.db).
Select("table_name").
From("INFORMATION_SCHEMA.TABLES").
Where(sq.Eq{
"table_name": tableName,
"table_schema": s.schemaName,
})
rows, err := query.Query()
if err != nil {
s.logger.Error(`doesTableExist ERROR`, mlog.Err(err))
return false, err
}
defer s.CloseRows(rows)
exists := rows.Next()
sql, _, _ := query.ToSql()
s.logger.Trace("doesTableExist",
mlog.String("table", tableName),
mlog.Bool("exists", exists),
mlog.String("sql", sql),
)
return exists, nil
}
func (s *SQLStore) doesColumnExist(tableName, columnName string) (bool, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
query := s.getQueryBuilder(s.db).
Select("table_name").
From("INFORMATION_SCHEMA.COLUMNS").
Where(sq.Eq{
"table_name": tableName,
"table_schema": s.schemaName,
"column_name": columnName,
})
rows, err := query.Query()
if err != nil {
s.logger.Error(`doesColumnExist ERROR`, mlog.Err(err))
return false, err
}
defer s.CloseRows(rows)
exists := rows.Next()
sql, _, _ := query.ToSql()
s.logger.Trace("doesColumnExist",
mlog.String("table", tableName),
mlog.String("column", columnName),
mlog.Bool("exists", exists),
mlog.String("sql", sql),
)
return exists, nil
}
func (s *SQLStore) genAddConstraintIfNeeded(tableName, constraintName, constraintType, constraintDefinition string) (string, error) {
tableName = addPrefixIfNeeded(tableName, s.tablePrefix)
normTableName := normalizeTablename(s.schemaName, tableName)
var query string
vars := map[string]string{
"schema": s.schemaName,
"constraint_name": constraintName,
"constraint_type": constraintType,
"table_name": tableName,
"constraint_definition": constraintDefinition,
"norm_table_name": normTableName,
}
switch s.dbType {
case model.MysqlDBType:
query = replaceVars(`
SET @stmt = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE constraint_schema = '[[schema]]'
AND constraint_name = '[[constraint_name]]'
AND constraint_type = '[[constraint_type]]'
AND table_name = '[[table_name]]'
) > 0,
'SELECT 1;',
'ALTER TABLE [[norm_table_name]] ADD CONSTRAINT [[constraint_name]] [[constraint_definition]];'
));
PREPARE addConstraintIfNeeded FROM @stmt;
EXECUTE addConstraintIfNeeded;
DEALLOCATE PREPARE addConstraintIfNeeded;
`, vars)
case model.PostgresDBType:
query = replaceVars(`
DO
$$
BEGIN
IF NOT EXISTS (
SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE constraint_schema = '[[schema]]'
AND constraint_name = '[[constraint_name]]'
AND constraint_type = '[[constraint_type]]'
AND table_name = '[[table_name]]'
) THEN
ALTER TABLE [[norm_table_name]] ADD CONSTRAINT [[constraint_name]] [[constraint_definition]];
END IF;
END;
$$
LANGUAGE plpgsql;
`, vars)
}
return query, nil
}
func addPrefixIfNeeded(s, prefix string) string {
if !strings.HasPrefix(s, prefix) {
return prefix + s
}
return s
}
func normalizeTablename(schemaName, tableName string) string {
if schemaName != "" && !strings.HasPrefix(tableName, schemaName+".") {
tableName = schemaName + "." + tableName
}
return tableName
}
func getIndexName(tableName string, columns string) string {
var sb strings.Builder
_, _ = sb.WriteString("idx_")
_, _ = sb.WriteString(tableName)
// allow developers to separate column names with spaces and/or commas
columns = strings.ReplaceAll(columns, ",", " ")
cols := strings.Split(columns, " ")
for _, s := range cols {
sub := strings.TrimSpace(s)
if sub == "" {
continue
}
_, _ = sb.WriteString("_")
_, _ = sb.WriteString(s)
}
return sb.String()
}
// replaceVars replaces instances of variable placeholders with the
// values provided via a map. Variable placeholders are of the form
// `[[var_name]]`.
func replaceVars(s string, vars map[string]string) string {
for key, val := range vars {
placeholder := "[[" + key + "]]"
val = strings.ReplaceAll(val, "'", "\\'")
s = strings.ReplaceAll(s, placeholder, val)
}
return s
}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}blocks (
id VARCHAR(36),
{{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}}
{{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}}
parent_id VARCHAR(36),
{{if .mysql}}`schema`{{else}}schema{{end}} BIGINT,
type TEXT,
title TEXT,
fields {{if .postgres}}JSON{{else}}TEXT{{end}},
create_at BIGINT,
update_at BIGINT,
delete_at BIGINT,
PRIMARY KEY (id, insert_at)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}system_settings (
id VARCHAR(100),
value TEXT,
PRIMARY KEY (id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,2 @@
{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}}
{{ addColumnIfNeeded "blocks" "root_id" "varchar(36)" ""}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}users (
id VARCHAR(100),
username VARCHAR(100),
email VARCHAR(255),
password VARCHAR(100),
mfa_secret VARCHAR(100),
auth_service VARCHAR(20),
auth_data VARCHAR(255),
props {{if .postgres}}JSON{{else}}TEXT{{end}},
create_at BIGINT,
update_at BIGINT,
delete_at BIGINT,
PRIMARY KEY (id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
CREATE TABLE IF NOT EXISTS {{.prefix}}sessions (
id VARCHAR(100),
token VARCHAR(100),
user_id VARCHAR(100),
props {{if .postgres}}JSON{{else}}TEXT{{end}},
create_at BIGINT,
update_at BIGINT,
PRIMARY KEY (id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,2 @@
{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}}
{{ addColumnIfNeeded "blocks" "modified_by" "varchar(36)" ""}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}sharing (
id VARCHAR(36),
enabled BOOLEAN,
token VARCHAR(100),
modified_by VARCHAR(36),
update_at BIGINT,
PRIMARY KEY (id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}workspaces (
id VARCHAR(36),
signup_token VARCHAR(100) NOT NULL,
settings {{if .postgres}}JSON{{else}}TEXT{{end}},
modified_by VARCHAR(36),
update_at BIGINT,
PRIMARY KEY (id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,8 @@
{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}}
{{ addColumnIfNeeded "blocks" "workspace_id" "varchar(36)" ""}}
{{ addColumnIfNeeded "sharing" "workspace_id" "varchar(36)" ""}}
{{ addColumnIfNeeded "sessions" "auth_service" "varchar(20)" ""}}
UPDATE {{.prefix}}blocks SET workspace_id = '0' WHERE workspace_id = '' OR workspace_id IS NULL;

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,40 @@
{{- /* Only perform this migration if the blocks_history table does not already exist */ -}}
{{- /* doesTableExist tableName */ -}}
{{if doesTableExist "blocks_history" }}
SELECT 1;
{{else}}
{{- /* renameTableIfNeeded oldTableName newTableName */ -}}
{{ renameTableIfNeeded "blocks" "blocks_history" }}
CREATE TABLE IF NOT EXISTS {{.prefix}}blocks (
id VARCHAR(36),
{{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}}
{{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}}
parent_id VARCHAR(36),
{{if .mysql}}`schema`{{else}}schema{{end}} BIGINT,
type TEXT,
title TEXT,
fields {{if .postgres}}JSON{{else}}TEXT{{end}},
create_at BIGINT,
update_at BIGINT,
delete_at BIGINT,
root_id VARCHAR(36),
modified_by VARCHAR(36),
workspace_id VARCHAR(36),
PRIMARY KEY (workspace_id,id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
{{if .mysql}}
INSERT IGNORE INTO {{.prefix}}blocks (SELECT * FROM {{.prefix}}blocks_history ORDER BY insert_at DESC);
{{end}}
{{if .postgres}}
INSERT INTO {{.prefix}}blocks (SELECT * FROM {{.prefix}}blocks_history ORDER BY insert_at DESC) ON CONFLICT DO NOTHING;
{{end}}
{{end}}
DELETE FROM {{.prefix}}blocks where delete_at > 0;

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,7 @@
{{- /* addColumnIfNeeded tableName columnName datatype constraint) */ -}}
{{ addColumnIfNeeded "blocks" "created_by" "varchar(36)" ""}}
{{ addColumnIfNeeded "blocks_history" "created_by" "varchar(36)" ""}}
UPDATE {{.prefix}}blocks SET created_by =
COALESCE(NULLIF((select modified_by from {{.prefix}}blocks_history where {{.prefix}}blocks_history.id = {{.prefix}}blocks.id ORDER BY {{.prefix}}blocks_history.insert_at ASC limit 1), ''), 'system')
WHERE created_by IS NULL;

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,7 @@
{{- /* All tables have collation fixed via code at startup so this migration is no longer needed. */ -}}
{{- /* See https://github.com/mattermost/focalboard/pull/4002 */ -}}
SELECT 1;

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,54 @@
{{if and .mysql .plugin}}
-- this migration applies collation on column level.
-- collation of mattermost's Channels table
SET @mattermostCollation = (SELECT table_collation from information_schema.tables WHERE table_name = 'Channels' AND table_schema = (SELECT DATABASE()));
-- charset of mattermost's CHannels table's Name column
SET @mattermostCharset = (SELECT CHARACTER_SET_NAME from information_schema.columns WHERE table_name = 'Channels' AND table_schema = (SELECT DATABASE()) AND COLUMN_NAME = 'Name');
-- blocks
SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}blocks CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation);
PREPARE stmt FROM @updateCollationQuery;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- blocks history
SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}blocks_history CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation);
PREPARE stmt FROM @updateCollationQuery;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- sessions
SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}sessions CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation);
PREPARE stmt FROM @updateCollationQuery;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- sharing
SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}sharing CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation);
PREPARE stmt FROM @updateCollationQuery;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- system settings
SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}system_settings CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation);
PREPARE stmt FROM @updateCollationQuery;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- users
SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}users CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation);
PREPARE stmt FROM @updateCollationQuery;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- workspaces
SET @updateCollationQuery = CONCAT('ALTER TABLE {{.prefix}}workspaces CONVERT TO CHARACTER SET ', @mattermostCharset, ' COLLATE ', @mattermostCollation);
PREPARE stmt FROM @updateCollationQuery;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
{{else}}
-- We need a query here otherwise the migration will result
-- in an empty query when the if condition is false.
-- Empty query causes a "Query was empty" error.
SELECT 1;
{{end}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,18 @@
UPDATE {{.prefix}}users SET create_at = create_at*1000, update_at = update_at*1000, delete_at = delete_at*1000
WHERE create_at < 1000000000000;
UPDATE {{.prefix}}blocks SET create_at = create_at*1000, update_at = update_at*1000, delete_at = delete_at*1000
WHERE create_at < 1000000000000;
UPDATE {{.prefix}}blocks_history SET create_at = create_at*1000, update_at = update_at*1000, delete_at = delete_at*1000
WHERE create_at < 1000000000000;
UPDATE {{.prefix}}workspaces SET update_at = update_at*1000
WHERE update_at < 1000000000000;
UPDATE {{.prefix}}sharing SET update_at = update_at*1000
WHERE update_at < 1000000000000;
UPDATE {{.prefix}}sessions SET create_at = create_at*1000, update_at = update_at*1000
WHERE create_at < 1000000000000;

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,12 @@
UPDATE {{.prefix}}blocks SET created_by = 'system' where created_by IS NULL;
UPDATE {{.prefix}}blocks SET modified_by = 'system' where modified_by IS NULL;
{{if .mysql}}
ALTER TABLE {{.prefix}}blocks MODIFY created_by varchar(36) NOT NULL;
ALTER TABLE {{.prefix}}blocks MODIFY modified_by varchar(36) NOT NULL;
{{end}}
{{if .postgres}}
ALTER TABLE {{.prefix}}blocks ALTER COLUMN created_by set NOT NULL;
ALTER TABLE {{.prefix}}blocks ALTER COLUMN modified_by set NOT NULL;
{{end}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,105 @@
{{if .mysql}}
UPDATE {{.prefix}}blocks_history AS bh SET bh.parent_id='' WHERE bh.parent_id IS NULL;
UPDATE {{.prefix}}blocks_history AS bh SET bh.schema=1 WHERE bh.schema IS NULL;
UPDATE {{.prefix}}blocks_history AS bh SET bh.type='' WHERE bh.type IS NULL;
UPDATE {{.prefix}}blocks_history AS bh SET bh.title='' WHERE bh.title IS NULL;
UPDATE {{.prefix}}blocks_history AS bh SET bh.fields='' WHERE bh.fields IS NULL;
UPDATE {{.prefix}}blocks_history AS bh SET bh.create_at=0 WHERE bh.create_at IS NULL;
UPDATE {{.prefix}}blocks_history AS bh SET bh.root_id='' WHERE bh.root_id IS NULL;
UPDATE {{.prefix}}blocks_history AS bh SET bh.created_by='system' WHERE bh.created_by IS NULL;
{{else}}
/* parent_id */
UPDATE {{.prefix}}blocks_history AS bh1
SET parent_id = COALESCE(
(SELECT bh2.parent_id
FROM {{.prefix}}blocks_history AS bh2
WHERE bh1.id = bh2.id AND bh2.parent_id IS NOT NULL
ORDER BY bh2.insert_at ASC limit 1)
, '')
WHERE parent_id IS NULL;
/* schema */
UPDATE {{.prefix}}blocks_history AS bh1
SET schema = COALESCE(
(SELECT bh2.schema
FROM {{.prefix}}blocks_history AS bh2
WHERE bh1.id = bh2.id AND bh2.schema IS NOT NULL
ORDER BY bh2.insert_at ASC limit 1)
, 1)
WHERE schema IS NULL;
/* type */
UPDATE {{.prefix}}blocks_history AS bh1
SET type = COALESCE(
(SELECT bh2.type
FROM {{.prefix}}blocks_history AS bh2
WHERE bh1.id = bh2.id AND bh2.type IS NOT NULL
ORDER BY bh2.insert_at ASC limit 1)
, '')
WHERE type IS NULL;
/* title */
UPDATE {{.prefix}}blocks_history AS bh1
SET title = COALESCE(
(SELECT bh2.title
FROM {{.prefix}}blocks_history AS bh2
WHERE bh1.id = bh2.id AND bh2.title IS NOT NULL
ORDER BY bh2.insert_at ASC limit 1)
, '')
WHERE title IS NULL;
/* fields */
{{if .postgres}}
UPDATE {{.prefix}}blocks_history AS bh1
SET fields = COALESCE(
(SELECT bh2.fields
FROM {{.prefix}}blocks_history AS bh2
WHERE bh1.id = bh2.id AND bh2.fields IS NOT NULL
ORDER BY bh2.insert_at ASC limit 1)
, '{}'::json)
WHERE fields IS NULL;
{{else}}
UPDATE {{.prefix}}blocks_history AS bh1
SET fields = COALESCE(
(SELECT bh2.fields
FROM {{.prefix}}blocks_history AS bh2
WHERE bh1.id = bh2.id AND bh2.fields IS NOT NULL
ORDER BY bh2.insert_at ASC limit 1)
, '')
WHERE fields IS NULL;
{{end}}
/* create_at */
UPDATE {{.prefix}}blocks_history AS bh1
SET create_at = COALESCE(
(SELECT bh2.create_at
FROM {{.prefix}}blocks_history AS bh2
WHERE bh1.id = bh2.id AND bh2.create_at IS NOT NULL
ORDER BY bh2.insert_at ASC limit 1)
, bh1.update_at)
WHERE create_at IS NULL;
/* root_id */
UPDATE {{.prefix}}blocks_history AS bh1
SET root_id = COALESCE(
(SELECT bh2.root_id
FROM {{.prefix}}blocks_history AS bh2
WHERE bh1.id = bh2.id AND bh2.root_id IS NOT NULL
ORDER BY bh2.insert_at ASC limit 1)
, '')
WHERE root_id IS NULL;
/* created_by */
UPDATE {{.prefix}}blocks_history AS bh1
SET created_by = COALESCE(
(SELECT bh2.created_by
FROM {{.prefix}}blocks_history AS bh2
WHERE bh1.id = bh2.id AND bh2.created_by IS NOT NULL
ORDER BY bh2.insert_at ASC limit 1)
, 'system')
WHERE created_by IS NULL;
{{end}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}subscriptions (
block_type VARCHAR(10),
block_id VARCHAR(36),
workspace_id VARCHAR(36),
subscriber_type VARCHAR(10),
subscriber_id VARCHAR(36),
notified_at BIGINT,
create_at BIGINT,
delete_at BIGINT,
PRIMARY KEY (block_id, subscriber_id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
CREATE TABLE IF NOT EXISTS {{.prefix}}notification_hints (
block_type VARCHAR(10),
block_id VARCHAR(36),
workspace_id VARCHAR(36),
modified_by_id VARCHAR(36),
create_at BIGINT,
notify_at BIGINT,
PRIMARY KEY (block_id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}file_info (
id varchar(26) NOT NULL,
create_at BIGINT NOT NULL,
delete_at BIGINT,
name TEXT NOT NULL,
extension VARCHAR(50) NOT NULL,
size BIGINT NOT NULL,
archived BOOLEAN
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,288 @@
{{- /* renameTableIfNeeded oldTableName newTableName string */ -}}
{{ renameTableIfNeeded "workspaces" "teams" }}
{{- /* renameColumnIfNeeded tableName oldColumnName newColumnName dataType */ -}}
{{ renameColumnIfNeeded "blocks" "workspace_id" "channel_id" "varchar(36)" }}
{{ renameColumnIfNeeded "blocks_history" "workspace_id" "channel_id" "varchar(36)" }}
{{- /* dropColumnIfNeeded tableName columnName */ -}}
{{ dropColumnIfNeeded "blocks" "workspace_id" }}
{{ dropColumnIfNeeded "blocks_history" "workspace_id" }}
{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}}
{{ addColumnIfNeeded "blocks" "board_id" "varchar(36)" ""}}
{{ addColumnIfNeeded "blocks_history" "board_id" "varchar(36)" ""}}
{{- /* cleanup incorrect data format in column calculations */ -}}
{{- /* then move from 'board' type to 'view' type*/ -}}
{{if .mysql}}
UPDATE {{.prefix}}blocks SET fields = JSON_SET(fields, '$.columnCalculations', JSON_OBJECT()) WHERE JSON_EXTRACT(fields, '$.columnCalculations') = JSON_ARRAY();
UPDATE {{.prefix}}blocks b
JOIN (
SELECT id, JSON_EXTRACT(fields, '$.columnCalculations') as board_calculations from {{.prefix}}blocks
WHERE JSON_EXTRACT(fields, '$.columnCalculations') <> JSON_OBJECT()
) AS s on s.id = b.root_id
SET fields = JSON_SET(fields, '$.columnCalculations', JSON_ARRAY(s.board_calculations))
WHERE JSON_EXTRACT(b.fields, '$.viewType') = 'table'
AND b.type = 'view';
{{end}}
{{if .postgres}}
UPDATE {{.prefix}}blocks SET fields = fields::jsonb - 'columnCalculations' || '{"columnCalculations": {}}' WHERE fields->>'columnCalculations' = '[]';
WITH subquery AS (
SELECT id, fields->'columnCalculations' as board_calculations from {{.prefix}}blocks
WHERE fields ->> 'columnCalculations' <> '{}')
UPDATE {{.prefix}}blocks b
SET fields = b.fields::jsonb|| json_build_object('columnCalculations', s.board_calculations::jsonb)::jsonb
FROM subquery AS s
WHERE s.id = b.root_id
AND b.fields ->> 'viewType' = 'table'
AND b.type = 'view';
{{end}}
{{- /* TODO: Migrate the columnCalculations at app level and remove it from the boards and boards_history tables */ -}}
{{- /* add boards tables */ -}}
CREATE TABLE IF NOT EXISTS {{.prefix}}boards (
id VARCHAR(36) NOT NULL PRIMARY KEY,
{{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}}
{{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}}
team_id VARCHAR(36) NOT NULL,
channel_id VARCHAR(36),
created_by VARCHAR(36),
modified_by VARCHAR(36),
type VARCHAR(1) NOT NULL,
title TEXT NOT NULL,
description TEXT,
icon VARCHAR(256),
show_description BOOLEAN,
is_template BOOLEAN,
template_version INT DEFAULT 0,
{{if .mysql}}
properties JSON,
card_properties JSON,
{{end}}
{{if .postgres}}
properties JSONB,
card_properties JSONB,
{{end}}
create_at BIGINT,
update_at BIGINT,
delete_at BIGINT
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
{{- /* createIndexIfNeeded tableName columns */ -}}
{{ createIndexIfNeeded "boards" "team_id, is_template" }}
{{ createIndexIfNeeded "boards" "channel_id" }}
CREATE TABLE IF NOT EXISTS {{.prefix}}boards_history (
id VARCHAR(36) NOT NULL,
{{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}}
{{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}}
team_id VARCHAR(36) NOT NULL,
channel_id VARCHAR(36),
created_by VARCHAR(36),
modified_by VARCHAR(36),
type VARCHAR(1) NOT NULL,
title TEXT NOT NULL,
description TEXT,
icon VARCHAR(256),
show_description BOOLEAN,
is_template BOOLEAN,
template_version INT DEFAULT 0,
{{if .mysql}}
properties JSON,
card_properties JSON,
{{end}}
{{if .postgres}}
properties JSONB,
card_properties JSONB,
{{end}}
create_at BIGINT,
update_at BIGINT,
delete_at BIGINT,
PRIMARY KEY (id, insert_at)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
{{- /* migrate board blocks to boards table */ -}}
{{if .plugin}}
{{if .postgres}}
INSERT INTO {{.prefix}}boards (
SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.type,
COALESCE(B.title, ''),
COALESCE((B.fields->>'description')::text, ''),
B.fields->>'icon',
COALESCE((fields->'showDescription')::text::boolean, false),
COALESCE((fields->'isTemplate')::text::boolean, false),
COALESCE((B.fields->'templateVer')::text::int, 0),
'{}', B.fields->'cardProperties', B.create_at,
B.update_at, B.delete_at {{if doesColumnExist "boards" "minimum_role"}} ,'' {{end}}
FROM {{.prefix}}blocks AS B
INNER JOIN channels AS C ON C.Id=B.channel_id
WHERE B.type='board'
);
INSERT INTO {{.prefix}}boards_history (
SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.type,
COALESCE(B.title, ''),
COALESCE((B.fields->>'description')::text, ''),
B.fields->>'icon',
COALESCE((fields->'showDescription')::text::boolean, false),
COALESCE((fields->'isTemplate')::text::boolean, false),
COALESCE((B.fields->'templateVer')::text::int, 0),
'{}', B.fields->'cardProperties', B.create_at,
B.update_at, B.delete_at {{if doesColumnExist "boards_history" "minimum_role"}} ,'' {{end}}
FROM {{.prefix}}blocks_history AS B
INNER JOIN channels AS C ON C.Id=B.channel_id
WHERE B.type='board'
);
{{end}}
{{if .mysql}}
INSERT INTO {{.prefix}}boards (
SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.Type,
COALESCE(B.title, ''),
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), ''),
JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.icon')),
COALESCE(JSON_EXTRACT(B.fields, '$.showDescription'), 'false') = 'true',
COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true',
COALESCE(JSON_EXTRACT(B.fields, '$.templateVer'), 0),
'{}', JSON_EXTRACT(B.fields, '$.cardProperties'), B.create_at,
B.update_at, B.delete_at {{if doesColumnExist "boards" "minimum_role"}} ,'' {{end}}
FROM {{.prefix}}blocks AS B
INNER JOIN Channels AS C ON C.Id=B.channel_id
WHERE B.type='board'
);
INSERT INTO {{.prefix}}boards_history (
SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.Type,
COALESCE(B.title, ''),
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), ''),
JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.icon')),
COALESCE(JSON_EXTRACT(B.fields, '$.showDescription'), 'false') = 'true',
COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true',
COALESCE(JSON_EXTRACT(B.fields, '$.templateVer'), 0),
'{}', JSON_EXTRACT(B.fields, '$.cardProperties'), B.create_at,
B.update_at, B.delete_at {{if doesColumnExist "boards_history" "minimum_role"}} ,'' {{end}}
FROM {{.prefix}}blocks_history AS B
INNER JOIN Channels AS C ON C.Id=B.channel_id
WHERE B.type='board'
);
{{end}}
{{else}}
{{if .postgres}}
INSERT INTO {{.prefix}}boards (
SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O',
COALESCE(B.title, ''),
COALESCE((fields->>'description')::text, ''),
B.fields->>'icon',
COALESCE((fields->'showDescription')::text::boolean, false),
COALESCE((fields->'isTemplate')::text::boolean, false),
COALESCE((B.fields->'templateVer')::text::int, 0),
'{}', fields->'cardProperties', create_at,
update_at, delete_at {{if doesColumnExist "boards" "minimum_role"}} ,'editor' {{end}}
FROM {{.prefix}}blocks AS B
WHERE type='board'
);
INSERT INTO {{.prefix}}boards_history (
SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O',
COALESCE(B.title, ''),
COALESCE((fields->>'description')::text, ''),
B.fields->>'icon',
COALESCE((fields->'showDescription')::text::boolean, false),
COALESCE((fields->'isTemplate')::text::boolean, false),
COALESCE((B.fields->'templateVer')::text::int, 0),
'{}', fields->'cardProperties', create_at,
update_at, delete_at {{if doesColumnExist "boards_history" "minimum_role"}} ,'editor' {{end}}
FROM {{.prefix}}blocks_history AS B
WHERE type='board'
);
{{end}}
{{if .mysql}}
INSERT INTO {{.prefix}}boards (
SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O',
COALESCE(B.title, ''),
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), ''),
JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')),
COALESCE(JSON_EXTRACT(B.fields, '$.showDescription'), 'false') = 'true',
COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true',
COALESCE(JSON_EXTRACT(B.fields, '$.templateVer'), 0),
'{}', JSON_EXTRACT(fields, '$.cardProperties'), create_at,
update_at, delete_at {{if doesColumnExist "boards" "minimum_role"}} ,'editor' {{end}}
FROM {{.prefix}}blocks AS B
WHERE type='board'
);
INSERT INTO {{.prefix}}boards_history (
SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O',
COALESCE(B.title, ''),
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(B.fields,'$.description')), ''),
JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')),
COALESCE(JSON_EXTRACT(B.fields, '$.showDescription'), 'false') = 'true',
COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true',
COALESCE(JSON_EXTRACT(B.fields, '$.templateVer'), 0),
'{}', JSON_EXTRACT(fields, '$.cardProperties'), create_at,
update_at, delete_at {{if doesColumnExist "boards_history" "minimum_role"}} ,'editor' {{end}}
FROM {{.prefix}}blocks_history AS B
WHERE type='board'
);
{{end}}
{{end}}
{{- /* Update block references to boards*/ -}}
UPDATE {{.prefix}}blocks SET board_id=root_id WHERE board_id IS NULL OR board_id='';
UPDATE {{.prefix}}blocks_history SET board_id=root_id WHERE board_id IS NULL OR board_id='';
{{- /* Remove boards, including templates */ -}}
DELETE FROM {{.prefix}}blocks WHERE type = 'board';
DELETE FROM {{.prefix}}blocks_history WHERE type = 'board';
{{- /* add board_members (only if boards_members doesn't already exist) */ -}}
{{if not (doesTableExist "board_members") }}
CREATE TABLE IF NOT EXISTS {{.prefix}}board_members (
board_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
roles VARCHAR(64),
scheme_admin BOOLEAN,
scheme_editor BOOLEAN,
scheme_commenter BOOLEAN,
scheme_viewer BOOLEAN,
PRIMARY KEY (board_id, user_id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
{{- /* if we're in plugin, migrate channel memberships to the board */ -}}
{{if .plugin}}
INSERT INTO {{.prefix}}board_members (
SELECT B.Id, CM.UserId, CM.Roles, TRUE, TRUE, FALSE, FALSE
FROM {{.prefix}}boards AS B
INNER JOIN ChannelMembers as CM ON CM.ChannelId=B.channel_id
WHERE CM.SchemeAdmin=True OR (CM.UserId=B.created_by)
);
{{end}}
{{- /* if we're in personal server or desktop, create memberships for everyone */ -}}
{{if and (not .plugin) (not .singleUser)}}
{{- /* for personal server, create a membership per user and board */ -}}
INSERT INTO {{.prefix}}board_members
SELECT B.id, U.id, '', B.created_by=U.id, TRUE, FALSE, FALSE
FROM {{.prefix}}boards AS B, {{.prefix}}users AS U;
{{end}}
{{if and (not .plugin) .singleUser}}
{{- /* for personal desktop, as we don't have users, create a membership */ -}}
{{- /* per board with a fixed user id */ -}}
INSERT INTO {{.prefix}}board_members
SELECT B.id, 'single-user', '', TRUE, TRUE, FALSE, FALSE
FROM {{.prefix}}boards AS B;
{{end}}
{{end}}
{{- /* createIndexIfNeeded tableName columns */ -}}
{{ createIndexIfNeeded "board_members" "user_id" }}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}categories (
id varchar(36) NOT NULL,
name varchar(100) NOT NULL,
user_id varchar(36) NOT NULL,
team_id varchar(36) NOT NULL,
channel_id varchar(36),
create_at BIGINT,
update_at BIGINT,
delete_at BIGINT,
PRIMARY KEY (id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
{{- /* createIndexIfNeeded tableName columns */ -}}
{{ createIndexIfNeeded "categories" "user_id, team_id" }}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}category_boards (
id varchar(36) NOT NULL,
user_id varchar(36) NOT NULL,
category_id varchar(36) NOT NULL,
board_id VARCHAR(36) NOT NULL,
create_at BIGINT,
update_at BIGINT,
delete_at BIGINT,
PRIMARY KEY (id)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
{{- /* createIndexIfNeeded tableName columns */ -}}
{{ createIndexIfNeeded "category_boards" "category_id" }}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,23 @@
{{- /* Only perform this migration if the board_members_history table does not already exist */ -}}
{{if doesTableExist "board_members_history" }}
SELECT 1;
{{else}}
CREATE TABLE IF NOT EXISTS {{.prefix}}board_members_history (
board_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
action VARCHAR(10),
{{if .postgres}}insert_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),{{end}}
{{if .mysql}}insert_at DATETIME(6) NOT NULL DEFAULT NOW(6),{{end}}
PRIMARY KEY (board_id, user_id, insert_at)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
INSERT INTO {{.prefix}}board_members_history (board_id, user_id, action) SELECT board_id, user_id, 'created' from {{.prefix}}board_members;
{{end}}
{{- /* createIndexIfNeeded tableName columns */ -}}
{{ createIndexIfNeeded "board_members_history" "user_id" }}
{{ createIndexIfNeeded "board_members_history" "board_id, user_id" }}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,6 @@
{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}}
{{ addColumnIfNeeded "boards" "minimum_role" "varchar(36)" "NOT NULL DEFAULT ''"}}
{{ addColumnIfNeeded "boards_history" "minimum_role" "varchar(36)" "NOT NULL DEFAULT ''"}}
UPDATE {{.prefix}}boards SET minimum_role = 'editor' WHERE minimum_role IS NULL OR minimum_role='';
UPDATE {{.prefix}}boards_history SET minimum_role = 'editor' WHERE minimum_role IS NULL OR minimum_role='';

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,2 @@
{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}}
{{ addColumnIfNeeded "categories" "collapsed" "boolean" "default false"}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1 @@
UPDATE {{.prefix}}categories SET collapsed = true;

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,16 @@
{{- /* delete old blocks PK and add id as the new one */ -}}
{{if .mysql}}
ALTER TABLE {{.prefix}}blocks DROP PRIMARY KEY;
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id);
{{end}}
{{if .postgres}}
ALTER TABLE {{.prefix}}blocks DROP CONSTRAINT {{.prefix}}blocks_pkey1;
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id);
{{end}}
{{- /* most block searches use board_id or a combination of board and parent ids */ -}}
{{ createIndexIfNeeded "blocks" "board_id, parent_id" }}
{{- /* get subscriptions is used once per board page load */ -}}
{{ createIndexIfNeeded "subscriptions" "subscriber_id" }}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS {{.prefix}}preferences
(
userid VARCHAR(36) NOT NULL,
category VARCHAR(32) NOT NULL,
name VARCHAR(32) NOT NULL,
value TEXT NULL,
PRIMARY KEY (userid, category, name)
) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}};
{{- /* createIndexIfNeeded tableName columns */ -}}
{{ createIndexIfNeeded "preferences" "category" }}
{{ createIndexIfNeeded "preferences" "name" }}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,54 @@
{{if .plugin}}
{{- /* For plugin mode, we need to write into Mattermost's `Preferences` table, hence, no use of `prefix`. */ -}}
{{if .postgres}}
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'welcomePageViewed', replace((Props->'focalboard_welcomePageViewed')::varchar, '"', '') FROM Users WHERE Props->'focalboard_welcomePageViewed' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace((Props->'hiddenBoardIDs')::varchar, '"[', '['), ']"', ']'), '\"', '"') FROM Users WHERE Props->'hiddenBoardIDs' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'tourCategory', replace((Props->'focalboard_tourCategory')::varchar, '"', '') FROM Users WHERE Props->'focalboard_tourCategory' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStep', replace((Props->'focalboard_onboardingTourStep')::varchar, '"', '') FROM Users WHERE Props->'focalboard_onboardingTourStep' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStarted', replace((Props->'focalboard_onboardingTourStarted')::varchar, '"', '') FROM Users WHERE Props->'focalboard_onboardingTourStarted' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'version72MessageCanceled', replace((Props->'focalboard_version72MessageCanceled')::varchar, '"', '') FROM Users WHERE Props->'focalboard_version72MessageCanceled' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'lastWelcomeVersion', replace((Props->'focalboard_lastWelcomeVersion')::varchar, '"', '') FROM Users WHERE Props->'focalboard_lastWelcomeVersion' IS NOT NULL ON CONFLICT DO NOTHING;
UPDATE Users SET props = (props - 'focalboard_welcomePageViewed' - 'hiddenBoardIDs' - 'focalboard_tourCategory' - 'focalboard_onboardingTourStep' - 'focalboard_onboardingTourStarted' - 'focalboard_version72MessageCanceled' - 'focalboard_lastWelcomeVersion') WHERE jsonb_typeof(props) = 'object';
{{end}}
{{if .mysql}}
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'welcomePageViewed', replace(JSON_EXTRACT(Props, '$."focalboard_welcomePageViewed"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_welcomePageViewed') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace(JSON_EXTRACT(Props, '$."hiddenBoardIDs"'), '"[', '['), ']"', ']'), '\\"', '"') FROM Users WHERE JSON_EXTRACT(Props, '$.hiddenBoardIDs') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'tourCategory', replace(JSON_EXTRACT(Props, '$."focalboard_tourCategory"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_tourCategory') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStep', replace(JSON_EXTRACT(Props, '$."focalboard_onboardingTourStep"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_onboardingTourStep') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStarted', replace(JSON_EXTRACT(Props, '$."focalboard_onboardingTourStarted"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_onboardingTourStarted') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'version72MessageCanceled', replace(JSON_EXTRACT(Props, '$."focalboard_version72MessageCanceled"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_version72MessageCanceled') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO Preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'lastWelcomeVersion', replace(JSON_EXTRACT(Props, '$."focalboard_lastWelcomeVersion"'), '"', '') FROM Users WHERE JSON_EXTRACT(Props, '$.focalboard_lastWelcomeVersion') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
UPDATE Users SET Props = JSON_REMOVE(Props, '$."focalboard_welcomePageViewed"', '$."hiddenBoardIDs"', '$."focalboard_tourCategory"', '$."focalboard_onboardingTourStep"', '$."focalboard_onboardingTourStarted"', '$."focalboard_version72MessageCanceled"', '$."focalboard_lastWelcomeVersion"');
{{end}}
{{else}}
{{- /* For personal server, we need to write to Focalboard's preferences table, hence the use of `prefix`. */ -}}
{{if .postgres}}
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'welcomePageViewed', replace((Props->'focalboard_welcomePageViewed')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_welcomePageViewed' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace((Props->'hiddenBoardIDs')::varchar, '"[', '['), ']"', ']'), '\"', '"') from {{.prefix}}users WHERE Props->'hiddenBoardIDs' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'tourCategory', replace((Props->'focalboard_tourCategory')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_tourCategory' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStep', replace((Props->'focalboard_onboardingTourStep')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_onboardingTourStep' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStarted', replace((Props->'focalboard_onboardingTourStarted')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_onboardingTourStarted' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'version72MessageCanceled', replace((Props->'focalboard_version72MessageCanceled')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_version72MessageCanceled' IS NOT NULL ON CONFLICT DO NOTHING;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'lastWelcomeVersion', replace((Props->'focalboard_lastWelcomeVersion')::varchar, '"', '') from {{.prefix}}users WHERE Props->'focalboard_lastWelcomeVersion' IS NOT NULL ON CONFLICT DO NOTHING;
UPDATE {{.prefix}}users SET props = (props::jsonb - 'focalboard_welcomePageViewed' - 'hiddenBoardIDs' - 'focalboard_tourCategory' - 'focalboard_onboardingTourStep' - 'focalboard_onboardingTourStarted' - 'focalboard_version72MessageCanceled' - 'focalboard_lastWelcomeVersion')::json WHERE jsonb_typeof(props::jsonb) = 'object';
{{end}}
{{if .mysql}}
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'welcomePageViewed', replace(JSON_EXTRACT(Props, '$."focalboard_welcomePageViewed"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_welcomePageViewed') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace(JSON_EXTRACT(Props, '$."hiddenBoardIDs"'), '"[', '['), ']"', ']'), '\\"', '"') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.hiddenBoardIDs') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'tourCategory', replace(JSON_EXTRACT(Props, '$."focalboard_tourCategory"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_tourCategory') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStep', replace(JSON_EXTRACT(Props, '$."focalboard_onboardingTourStep"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_onboardingTourStep') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'onboardingTourStarted', replace(JSON_EXTRACT(Props, '$."focalboard_onboardingTourStarted"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_onboardingTourStarted') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'version72MessageCanceled', replace(JSON_EXTRACT(Props, '$."focalboard_version72MessageCanceled"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_version72MessageCanceled') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
INSERT INTO {{.prefix}}preferences (UserId, Category, Name, Value) SELECT Id, 'focalboard', 'lastWelcomeVersion', replace(JSON_EXTRACT(Props, '$."focalboard_lastWelcomeVersion"'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(Props, '$.focalboard_lastWelcomeVersion') IS NOT NULL ON DUPLICATE KEY UPDATE value = value;
UPDATE {{.prefix}}users SET Props = JSON_REMOVE(Props, '$."focalboard_welcomePageViewed"', '$."hiddenBoardIDs"', '$."focalboard_tourCategory"', '$."focalboard_onboardingTourStep"', '$."focalboard_onboardingTourStarted"', '$."focalboard_version72MessageCanceled"', '$."focalboard_lastWelcomeVersion"');
{{end}}
{{end}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1 @@
UPDATE {{.prefix}}boards SET channel_id = '' WHERE is_template;

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,4 @@
{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}}
{{ addColumnIfNeeded "categories" "type" "varchar(64)" ""}}
UPDATE {{.prefix}}categories SET type = 'custom' WHERE type IS NULL;

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,2 @@
{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}}
{{ addColumnIfNeeded "categories" "sort_order" "BIGINT" ""}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,2 @@
{{- /* addColumnIfNeeded tableName columnName datatype constraint */ -}}
{{ addColumnIfNeeded "category_boards" "sort_order" "BIGINT" ""}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,15 @@
{{- /* To move Boards category to to the last value, we just need a relatively large value. */ -}}
{{- /* Assigning 10x total number of categories works perfectly. The sort_order is anyways updated */ -}}
{{- /* when the user manually DNDs a category. */ -}}
{{if .postgres}}
UPDATE {{.prefix}}categories SET sort_order = (10 * (SELECT COUNT(*) FROM {{.prefix}}categories)) WHERE lower(name) = 'boards';
{{end}}
{{if .mysql}}
{{- /* MySQL doesn't allow referencing the same table in subquery and update query like Postgres, */ -}}
{{- /* So we save the subquery result in a variable to use later. */ -}}
SET @focalboard_numCategories = (SELECT COUNT(*) FROM {{.prefix}}categories);
UPDATE {{.prefix}}categories SET sort_order = (10 * @focalboard_numCategories) WHERE lower(name) = 'boards';
SET @focalboard_numCategories = NULL;
{{end}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1 @@
DELETE FROM {{.prefix}}category_boards WHERE delete_at > 0;

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,3 @@
{{ if or .postgres .mysql }}
{{ dropColumnIfNeeded "category_boards" "delete_at" }}
{{end}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1 @@
{{ addColumnIfNeeded "category_boards" "hidden" "boolean" "" }}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,3 @@
{{if or .mysql .postgres}}
{{ addConstraintIfNeeded "category_boards" "unique_user_category_board" "UNIQUE" "UNIQUE(user_id, board_id)"}}
{{end}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,41 @@
{{if .plugin}}
{{if .mysql}}
UPDATE {{.prefix}}category_boards AS fcb
JOIN Preferences p
ON fcb.user_id = p.userid
AND p.category = 'focalboard'
AND p.name = 'hiddenBoardIDs'
SET hidden = true
WHERE p.value LIKE concat('%', fcb.board_id, '%');
{{end}}
{{if .postgres}}
UPDATE {{.prefix}}category_boards as fcb
SET hidden = true
FROM preferences p
WHERE p.userid = fcb.user_id
AND p.category = 'focalboard'
AND p.name = 'hiddenBoardIDs'
AND p.value like ('%' || fcb.board_id || '%');
{{end}}
{{else}}
{{if .mysql}}
UPDATE {{.prefix}}category_boards AS fcb
JOIN {{.prefix}}preferences p
ON fcb.user_id = p.userid
AND p.category = 'focalboard'
AND p.name = 'hiddenBoardIDs'
SET hidden = true
WHERE p.value LIKE concat('%', fcb.board_id, '%');
{{end}}
{{if .postgres}}
UPDATE {{.prefix}}category_boards as fcb
SET hidden = true
FROM {{.prefix}}preferences p
WHERE p.userid = fcb.user_id
AND p.category = 'focalboard'
AND p.name = 'hiddenBoardIDs'
AND p.value like ('%' || fcb.board_id || '%');
{{end}}
{{end}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1,5 @@
{{if .plugin}}
DELETE FROM Preferences WHERE category = 'focalboard' AND name = 'hiddenBoardIDs';
{{else}}
DELETE FROM {{.prefix}}preferences WHERE category = 'focalboard' AND name = 'hiddenBoardIDs';
{{end}}

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

@@ -0,0 +1 @@
SELECT 1;

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

@@ -0,0 +1 @@
{{ addColumnIfNeeded "file_info" "path" "varchar(512)" "" }}

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

@@ -0,0 +1,68 @@
# Migration Scripts
These scripts are executed against the current database on server start-up. Any scripts previously executed are skipped, however these scripts are designed to be idempotent for Postgres and MySQL. To correct common problems with schema and data migrations the `focalboard_schema_migrations` table can be cleared of all records and the server restarted.
The following built-in variables are available:
| Name | Syntax | Description |
| ----- | ----- | ----- |
| schemaName | {{ .schemaName }} | Returns the database/schema name (e.g. `mattermost_`, `mattermost_test`, `public`, ...) |
| prefix | {{ .prefix }} | Returns the table name prefix (e.g. `focalbaord_`) |
| postgres | {{if .postgres }} ... {{end}} | Returns true if the current database is Postgres. |
| sqlite | {{if .sqlite }} ... {{end}} | Returns true if the current database is Sqlite3. |
| mysql | {{if .mysql }} ... {{end}} | Returns true if the current database is MySQL. |
| plugin | {{if .plugin }} ... {{end}} | Returns true if the server is currently running as a plugin (or product). In others words this is true if the server is not running as stand-alone or personal server. |
| singleUser | {{if .singleUser }} ... {{end}} | Returns true if the server is currently running in single user mode. |
To help with creating scripts that are idempotent some template functions have been added to the migration engine.
| Name | Syntax | Description |
| ----- | ----- | ----- |
| addColumnIfNeeded | {{ addColumnIfNeeded schemaName tableName columnName datatype constraint }} | Adds column to table only if column doesn't already exist. |
| dropColumnIfNeeded | {{ dropColumnIfNeeded schemaName tableName columnName }} | Drops column from table if the column exists. |
| createIndexIfNeeded | {{ createIndexIfNeeded schemaName tableName columns }} | Creates an index if it does not already exist. The index name follows the existing convention of using `idx_` plus the table name and all columns separated by underscores. |
| renameTableIfNeeded | {{ renameTableIfNeeded schemaName oldTableName newTableName }} | Renames the table if the new table name does not exist. |
| renameColumnIfNeeded | {{ renameColumnIfNeeded schemaName tableName oldVolumnName newColumnName datatype }} | Renames a column if the new column name does not exist. |
| doesTableExist | {{if doesTableExist schemaName tableName }} ... {{end}} | Returns true if the table exists. Typically used in a `if` statement to conditionally include a section of script. Currently the existence of the table is determined before any scripts are executed (limitation of Morph). |
| doesColumnExist | {{if doesTableExist schemaName tableName columnName }} ... {{end}} | Returns true if the column exists. Typically used in a `if` statement to conditionally include a section of script. Currently the existence of the column is determined before any scripts are executed (limitation of Morph). |
**Note, table names should not include table prefix or schema name.**
## Examples
```bash
{{ addColumnIfNeeded .schemaName "categories" "type" "varchar(64)" ""}}
{{ addColumnIfNeeded .schemaName "boards_history" "minimum_role" "varchar(36)" "NOT NULL DEFAULT ''"}}
```
```bash
{{ dropColumnIfNeeded .schemaName "blocks_history" "workspace_id" }}
```
```bash
{{ createIndexIfNeeded .schemaName "boards" "team_id, is_template" }}
```
```bash
{{ renameTableIfNeeded .schemaName "blocks" "blocks_history" }}
```
```bash
{{ renameColumnIfNeeded .schemaName "blocks_history" "workspace_id" "channel_id" "varchar(36)" }}
```
```bash
{{if doesTableExist .schemaName "blocks_history" }}
SELECT 'table exists';
{{end}}
{{if not (doesTableExist .schemaName "blocks_history") }}
SELECT 1;
{{end}}
```
```bash
{{if doesColumnExist .schemaName "boards_history" "minimum_role"}}
UPDATE ...
{{end}}
```

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

@@ -0,0 +1,51 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package migrationstests
import (
"testing"
"github.com/mattermost/mattermost-server/v6/server/boards/services/store/sqlstore"
"github.com/mgdelacroix/foundation"
"github.com/stretchr/testify/require"
)
func TestDeletedMembershipBoardsMigration(t *testing.T) {
sqlstore.RunStoreTestsWithFoundation(t, func(t *testing.T, f *foundation.Foundation) {
t.Run("should detect a board linked to a team in which the owner has a deleted membership and restore it", func(t *testing.T) {
th, tearDown := SetupTestHelper(t, f)
defer tearDown()
th.f.MigrateToStepSkippingLastInterceptor(18).
ExecFile("./fixtures/deletedMembershipBoardsMigrationFixtures.sql")
boardGroupChannel := struct {
Created_By string
Team_ID string
}{}
boardDirectMessage := struct {
Created_By string
Team_ID string
}{}
th.f.DB().Get(&boardGroupChannel, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
require.Equal(t, "user-one", boardGroupChannel.Created_By)
require.Equal(t, "team-one", boardGroupChannel.Team_ID)
th.f.DB().Get(&boardDirectMessage, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
require.Equal(t, "user-one", boardDirectMessage.Created_By)
require.Equal(t, "team-one", boardDirectMessage.Team_ID)
th.f.RunInterceptor(18)
th.f.DB().Get(&boardGroupChannel, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
require.Equal(t, "user-one", boardGroupChannel.Created_By)
require.Equal(t, "team-three", boardGroupChannel.Team_ID)
th.f.DB().Get(&boardDirectMessage, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
require.Equal(t, "user-one", boardDirectMessage.Created_By)
require.Equal(t, "team-three", boardDirectMessage.Team_ID)
})
})
}

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

@@ -0,0 +1,47 @@
INSERT INTO Teams
(Id, Name, Type, DeleteAt)
VALUES
('team-one', 'team-one', 'O', 0),
('team-two', 'team-two', 'O', 0),
('team-three', 'team-three', 'O', 0);
INSERT INTO Channels
(Id, DeleteAt, TeamId, Type, Name, CreatorId)
VALUES
('group-channel', 0, 'team-one', 'G', 'group-channel', 'user-one'),
('direct-channel', 0, 'team-one', 'D', 'direct-channel', 'user-one');
INSERT INTO Users
(Id, Username, Email)
VALUES
('user-one', 'john-doe', 'john-doe@sample.com'),
('user-two', 'jane-doe', 'jane-doe@sample.com');
INSERT INTO focalboard_boards
(id, team_id, channel_id, created_by, modified_by, type, title, description, icon, show_description, is_template, create_at, update_at, delete_at)
VALUES
('board-group-channel', 'team-one', 'group-channel', 'user-one', 'user-one', 'P', 'Group Channel Board', '', '', false, false, 123, 123, 0),
('board-direct-channel', 'team-one', 'direct-channel', 'user-one', 'user-one', 'P', 'Direct Channel Board', '', '', false, false, 123, 123, 0);
INSERT INTO focalboard_board_members
(board_id, user_id, scheme_admin)
VALUES
('board-group-channel', 'user-one', true),
('board-direct-channel', 'user-one', true);
INSERT INTO TeamMembers
(TeamId, UserId, DeleteAt, SchemeAdmin)
VALUES
('team-one', 'user-one', 123, true),
('team-one', 'user-two', 123, true),
('team-two', 'user-one', 123, true),
('team-two', 'user-two', 123, true),
('team-three', 'user-one', 0, true),
('team-three', 'user-two', 0, true);
INSERT INTO ChannelMembers
(ChannelId, UserId, SchemeUser, SchemeAdmin)
VALUES
('group-channel', 'user-one', true, true),
('group-channel', 'two-one', true, false),
('direct-channel', 'user-one', true, true);

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

@@ -0,0 +1,10 @@
INSERT INTO Channels (Id, CreateAt, UpdateAt, DeleteAt, TeamId, Type, Name, CreatorId) VALUES ('chan-id', 123, 123, 0, 'team-id', 'O', 'channel', 'user-id');
INSERT INTO focalboard_blocks
(id, workspace_id, root_id, parent_id, created_by, modified_by, type, title, create_at, update_at, delete_at, fields)
VALUES
('board-id', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'board', 'My Board', 123, 123, 0, '{"columnCalculations": {"__title":"countUniqueValue"}}'),
('card-id', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'card', 'A card', 123, 123, 0, '{}'),
('view-id', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'view', 'A view', 123, 123, 0, '{"viewType":"table"}'),
('view-id2', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'view', 'A view2', 123, 123, 0, '{"viewType":"board"}'),
('board-id2', 'chan-id', 'board-id2', 'board-id2', 'user-id', 'user-id', 'board', 'My Board Two', 123, 123, 0, '{"description": "My Description","showDescription":true,"isTemplate":true,"templateVer":1,"columnCalculations":[]}');

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

@@ -0,0 +1,5 @@
INSERT INTO focalboard_boards
(id, title, type, is_template, channel_id, team_id)
VALUES
('board-id', 'Board', 'O', false, 'linked-channel', 'team-id'),
('template-id', 'Template', 'O', true, 'linked-channel', 'team-id');

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

@@ -0,0 +1,6 @@
INSERT INTO focalboard_category_boards values
('id-1', 'user_id-1', 'category-id-1', 'board-id-1', 1672988834402, 1672988834402, 0, 0),
('id-2', 'user_id-1', 'category-id-2', 'board-id-1', 1672988834402, 1672988834402, 0, 0),
('id-3', 'user_id-2', 'category-id-3', 'board-id-2', 1672988834402, 1672988834402, 1672988834402, 0),
('id-4', 'user_id-2', 'category-id-3', 'board-id-4', 1672988834402, 1672988834402, 0, 0),
('id-5', 'user_id-3', 'category-id-4', 'board-id-3', 1672988834402, 1672988834402, 1672988834402, 0);

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше