Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
1042
server/boards/services/store/sqlstore/blocks.go
Обычный файл
1042
server/boards/services/store/sqlstore/blocks.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
905
server/boards/services/store/sqlstore/board.go
Обычный файл
905
server/boards/services/store/sqlstore/board.go
Обычный файл
@@ -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
|
||||
}
|
||||
154
server/boards/services/store/sqlstore/board_insights.go
Обычный файл
154
server/boards/services/store/sqlstore/board_insights.go
Обычный файл
@@ -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
|
||||
}
|
||||
187
server/boards/services/store/sqlstore/boards_and_blocks.go
Обычный файл
187
server/boards/services/store/sqlstore/boards_and_blocks.go
Обычный файл
@@ -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)
|
||||
}
|
||||
255
server/boards/services/store/sqlstore/boards_migrator.go
Обычный файл
255
server/boards/services/store/sqlstore/boards_migrator.go
Обычный файл
@@ -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
|
||||
}
|
||||
249
server/boards/services/store/sqlstore/category.go
Обычный файл
249
server/boards/services/store/sqlstore/category.go
Обычный файл
@@ -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
|
||||
}
|
||||
195
server/boards/services/store/sqlstore/category_boards.go
Обычный файл
195
server/boards/services/store/sqlstore/category_boards.go
Обычный файл
@@ -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
|
||||
}
|
||||
118
server/boards/services/store/sqlstore/cloud.go
Обычный файл
118
server/boards/services/store/sqlstore/cloud.go
Обычный файл
@@ -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)
|
||||
}
|
||||
245
server/boards/services/store/sqlstore/compliance.go
Обычный файл
245
server/boards/services/store/sqlstore/compliance.go
Обычный файл
@@ -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
|
||||
}
|
||||
887
server/boards/services/store/sqlstore/data_migrations.go
Обычный файл
887
server/boards/services/store/sqlstore/data_migrations.go
Обычный файл
@@ -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
|
||||
}
|
||||
265
server/boards/services/store/sqlstore/data_migrations_test.go
Обычный файл
265
server/boards/services/store/sqlstore/data_migrations_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
179
server/boards/services/store/sqlstore/data_retention.go
Обычный файл
179
server/boards/services/store/sqlstore/data_retention.go
Обычный файл
@@ -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
|
||||
}
|
||||
95
server/boards/services/store/sqlstore/file.go
Обычный файл
95
server/boards/services/store/sqlstore/file.go
Обычный файл
@@ -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
|
||||
}
|
||||
263
server/boards/services/store/sqlstore/legacy_blocks.go
Обычный файл
263
server/boards/services/store/sqlstore/legacy_blocks.go
Обычный файл
@@ -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...)
|
||||
}
|
||||
22
server/boards/services/store/sqlstore/main_test.go
Обычный файл
22
server/boards/services/store/sqlstore/main_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
655
server/boards/services/store/sqlstore/migrate.go
Обычный файл
655
server/boards/services/store/sqlstore/migrate.go
Обычный файл
@@ -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)" "" }}
|
||||
68
server/boards/services/store/sqlstore/migrations/README.md
Обычный файл
68
server/boards/services/store/sqlstore/migrations/README.md
Обычный файл
@@ -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);
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user