1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-24 13:43:12 +02:00
focalboard/server/services/store/sqlstore/data_migrations.go
2023-03-30 10:51:43 +02:00

891 lines
25 KiB
Go

package sqlstore
import (
"context"
"fmt"
"os"
"strconv"
sq "github.com/Masterminds/squirrel"
"github.com/wiggin77/merror"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/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 {
// not supported for SQLite
if s.dbType == model.SqliteDBType {
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
}
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 := "DELETE FROM " + s.tablePrefix + "category_boards WHERE id NOT IN " +
"(SELECT * FROM ( SELECT min(id) FROM " + s.tablePrefix + "category_boards GROUP BY user_id, board_id ) as data)"
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
}