Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1115 lines
34 KiB
Go
1115 lines
34 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package sqlstore
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/lib/pq"
|
|
sq "github.com/mattermost/squirrel"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
|
)
|
|
|
|
type SqlRetentionPolicyStore struct {
|
|
*SqlStore
|
|
metrics einterfaces.MetricsInterface
|
|
}
|
|
|
|
func newSqlRetentionPolicyStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.RetentionPolicyStore {
|
|
return &SqlRetentionPolicyStore{
|
|
SqlStore: sqlStore,
|
|
metrics: metrics,
|
|
}
|
|
}
|
|
|
|
// executePossiblyEmptyQuery only executes the query if it is non-empty. This helps avoid
|
|
// having to check for MySQL, which, unlike Postgres, does not allow empty queries.
|
|
func executePossiblyEmptyQuery(txn *sqlxTxWrapper, query string, args ...any) (sql.Result, error) {
|
|
if query == "" {
|
|
return nil, nil
|
|
}
|
|
return txn.Exec(query, args...)
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) Save(policy *model.RetentionPolicyWithTeamAndChannelIDs) (_ *model.RetentionPolicyWithTeamAndChannelCounts, err error) {
|
|
// Strategy:
|
|
// 1. Insert new policy
|
|
// 2. Insert new channels into policy
|
|
// 3. Insert new teams into policy
|
|
|
|
if err = s.checkTeamsExist(policy.TeamIDs); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = s.checkChannelsExist(policy.ChannelIDs); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
policy.ID = model.NewId()
|
|
|
|
policyInsertQuery, policyInsertArgs, err := s.getQueryBuilder().
|
|
Insert("RetentionPolicies").
|
|
Columns("Id", "DisplayName", "PostDuration").
|
|
Values(policy.ID, policy.DisplayName, policy.PostDurationDays).
|
|
ToSql()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
channelsInsertQuery, channelsInsertArgs, err := s.buildInsertRetentionPoliciesChannelsQuery(policy.ID, policy.ChannelIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
teamsInsertQuery, teamsInsertArgs, err := s.buildInsertRetentionPoliciesTeamsQuery(policy.ID, policy.TeamIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
queryString, args, err := s.buildGetPolicyQuery(policy.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
txn, err := s.GetMaster().Beginx()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer finalizeTransactionX(txn, &err)
|
|
|
|
// Create a new policy in RetentionPolicies
|
|
if _, err = txn.Exec(policyInsertQuery, policyInsertArgs...); err != nil {
|
|
return nil, err
|
|
}
|
|
// Insert the channel IDs into RetentionPoliciesChannels
|
|
if _, err = executePossiblyEmptyQuery(txn, channelsInsertQuery, channelsInsertArgs...); err != nil {
|
|
return nil, err
|
|
}
|
|
// Insert the team IDs into RetentionPoliciesTeams
|
|
if _, err = executePossiblyEmptyQuery(txn, teamsInsertQuery, teamsInsertArgs...); err != nil {
|
|
return nil, err
|
|
}
|
|
// Select the new policy (with team/channel counts) which we just created
|
|
var newPolicy model.RetentionPolicyWithTeamAndChannelCounts
|
|
|
|
if err = txn.Get(&newPolicy, queryString, args...); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = txn.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &newPolicy, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) checkTeamsExist(teamIDs []string) error {
|
|
if len(teamIDs) > 0 {
|
|
teamsSelectQuery, teamsSelectArgs, err := s.getQueryBuilder().
|
|
Select("Id").
|
|
From("Teams").
|
|
Where(sq.Eq{"Id": teamIDs}).
|
|
ToSql()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows := []*string{}
|
|
err = s.GetReplica().Select(&rows, teamsSelectQuery, teamsSelectArgs...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(rows) == len(teamIDs) {
|
|
return nil
|
|
}
|
|
retrievedIDs := make(map[string]bool)
|
|
for _, teamID := range rows {
|
|
retrievedIDs[*teamID] = true
|
|
}
|
|
for _, teamID := range teamIDs {
|
|
if _, ok := retrievedIDs[teamID]; !ok {
|
|
return store.NewErrNotFound("Team", teamID)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) checkChannelsExist(channelIDs []string) error {
|
|
if len(channelIDs) > 0 {
|
|
channelsSelectQuery, channelsSelectArgs, err := s.getQueryBuilder().
|
|
Select("Id").
|
|
From("Channels").
|
|
Where(sq.Eq{"Id": channelIDs}).
|
|
ToSql()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows := []*string{}
|
|
err = s.GetReplica().Select(&rows, channelsSelectQuery, channelsSelectArgs...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(rows) == len(channelIDs) {
|
|
return nil
|
|
}
|
|
retrievedIDs := make(map[string]bool)
|
|
for _, channelID := range rows {
|
|
retrievedIDs[*channelID] = true
|
|
}
|
|
for _, channelID := range channelIDs {
|
|
if _, ok := retrievedIDs[channelID]; !ok {
|
|
return store.NewErrNotFound("Channel", channelID)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) buildInsertRetentionPoliciesChannelsQuery(policyID string, channelIDs []string) (query string, args []any, err error) {
|
|
if len(channelIDs) > 0 {
|
|
builder := s.getQueryBuilder().
|
|
Insert("RetentionPoliciesChannels").
|
|
Columns("PolicyId", "ChannelId")
|
|
for _, channelID := range channelIDs {
|
|
builder = builder.Values(policyID, channelID)
|
|
}
|
|
query, args, err = builder.ToSql()
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) buildInsertRetentionPoliciesTeamsQuery(policyID string, teamIDs []string) (query string, args []any, err error) {
|
|
if len(teamIDs) > 0 {
|
|
builder := s.getQueryBuilder().
|
|
Insert("RetentionPoliciesTeams").
|
|
Columns("PolicyId", "TeamId")
|
|
for _, teamID := range teamIDs {
|
|
builder = builder.Values(policyID, teamID)
|
|
}
|
|
query, args, err = builder.ToSql()
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) Patch(patch *model.RetentionPolicyWithTeamAndChannelIDs) (_ *model.RetentionPolicyWithTeamAndChannelCounts, err error) {
|
|
// Strategy:
|
|
// 1. Update policy attributes
|
|
// 2. Delete existing channels from policy
|
|
// 3. Insert new channels into policy
|
|
// 4. Delete existing teams from policy
|
|
// 5. Insert new teams into policy
|
|
// 6. Read new policy
|
|
|
|
if err = s.checkTeamsExist(patch.TeamIDs); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = s.checkChannelsExist(patch.ChannelIDs); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
policyUpdateQuery := ""
|
|
policyUpdateArgs := []any{}
|
|
if patch.DisplayName != "" || patch.PostDurationDays != nil {
|
|
builder := s.getQueryBuilder().Update("RetentionPolicies")
|
|
if patch.DisplayName != "" {
|
|
builder = builder.Set("DisplayName", patch.DisplayName)
|
|
}
|
|
if patch.PostDurationDays != nil {
|
|
builder = builder.Set("PostDuration", *patch.PostDurationDays)
|
|
}
|
|
policyUpdateQuery, policyUpdateArgs, err = builder.
|
|
Where(sq.Eq{"Id": patch.ID}).
|
|
ToSql()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
channelsDeleteQuery := ""
|
|
channelsDeleteArgs := []any{}
|
|
channelsInsertQuery := ""
|
|
channelsInsertArgs := []any{}
|
|
if patch.ChannelIDs != nil {
|
|
channelsDeleteQuery, channelsDeleteArgs, err = s.getQueryBuilder().
|
|
Delete("RetentionPoliciesChannels").
|
|
Where(sq.Eq{"PolicyId": patch.ID}).
|
|
ToSql()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
channelsInsertQuery, channelsInsertArgs, err = s.buildInsertRetentionPoliciesChannelsQuery(patch.ID, patch.ChannelIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
teamsDeleteQuery := ""
|
|
teamsDeleteArgs := []any{}
|
|
teamsInsertQuery := ""
|
|
teamsInsertArgs := []any{}
|
|
if patch.TeamIDs != nil {
|
|
teamsDeleteQuery, teamsDeleteArgs, err = s.getQueryBuilder().
|
|
Delete("RetentionPoliciesTeams").
|
|
Where(sq.Eq{"PolicyId": patch.ID}).
|
|
ToSql()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
teamsInsertQuery, teamsInsertArgs, err = s.buildInsertRetentionPoliciesTeamsQuery(patch.ID, patch.TeamIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
queryString, args, err := s.buildGetPolicyQuery(patch.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
txn, err := s.GetMaster().Beginx()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer finalizeTransactionX(txn, &err)
|
|
|
|
// Update the fields of the policy in RetentionPolicies
|
|
if _, err = executePossiblyEmptyQuery(txn, policyUpdateQuery, policyUpdateArgs...); err != nil {
|
|
return nil, err
|
|
}
|
|
// Remove all channels from the policy in RetentionPoliciesChannels
|
|
if _, err = executePossiblyEmptyQuery(txn, channelsDeleteQuery, channelsDeleteArgs...); err != nil {
|
|
return nil, err
|
|
}
|
|
// Insert the new channels for the policy in RetentionPoliciesChannels
|
|
if _, err = executePossiblyEmptyQuery(txn, channelsInsertQuery, channelsInsertArgs...); err != nil {
|
|
return nil, err
|
|
}
|
|
// Remove all teams from the policy in RetentionPoliciesTeams
|
|
if _, err = executePossiblyEmptyQuery(txn, teamsDeleteQuery, teamsDeleteArgs...); err != nil {
|
|
return nil, err
|
|
}
|
|
// Insert the new teams for the policy in RetentionPoliciesTeams
|
|
if _, err = executePossiblyEmptyQuery(txn, teamsInsertQuery, teamsInsertArgs...); err != nil {
|
|
return nil, err
|
|
}
|
|
// Select the policy which we just updated
|
|
var newPolicy model.RetentionPolicyWithTeamAndChannelCounts
|
|
if err = txn.Get(&newPolicy, queryString, args...); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = txn.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &newPolicy, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) buildGetPolicyQuery(id string) (string, []any, error) {
|
|
return s.buildGetPoliciesQuery(id, 0, 1)
|
|
}
|
|
|
|
// buildGetPoliciesQuery builds a query to select information for the policy with the specified
|
|
// ID, or, if `id` is the empty string, from all policies. The results returned will be sorted by
|
|
// policy display name and ID.
|
|
func (s *SqlRetentionPolicyStore) buildGetPoliciesQuery(id string, offset, limit int) (string, []any, error) {
|
|
rpcSubQuery := s.getQueryBuilder().
|
|
Select("RetentionPolicies.Id, COUNT(RetentionPoliciesChannels.ChannelId) AS Count").
|
|
From("RetentionPolicies").
|
|
LeftJoin("RetentionPoliciesChannels ON RetentionPolicies.Id = RetentionPoliciesChannels.PolicyId").
|
|
GroupBy("RetentionPolicies.Id").
|
|
OrderBy("RetentionPolicies.DisplayName, RetentionPolicies.Id").
|
|
Limit(uint64(limit)).
|
|
Offset(uint64(offset))
|
|
|
|
if id != "" {
|
|
rpcSubQuery = rpcSubQuery.Where(sq.Eq{"RetentionPolicies.Id": id})
|
|
}
|
|
|
|
rpcSubQueryString, args, err := rpcSubQuery.ToSql()
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(err, "retention_policies_tosql")
|
|
}
|
|
|
|
rptSubQuery := s.getQueryBuilder().
|
|
Select("RetentionPolicies.Id, COUNT(RetentionPoliciesTeams.TeamId) AS Count").
|
|
From("RetentionPolicies").
|
|
LeftJoin("RetentionPoliciesTeams ON RetentionPolicies.Id = RetentionPoliciesTeams.PolicyId").
|
|
GroupBy("RetentionPolicies.Id").
|
|
OrderBy("RetentionPolicies.DisplayName, RetentionPolicies.Id").
|
|
Limit(uint64(limit)).
|
|
Offset(uint64(offset))
|
|
|
|
if id != "" {
|
|
rptSubQuery = rptSubQuery.Where(sq.Eq{"RetentionPolicies.Id": id})
|
|
}
|
|
|
|
rptSubQueryString, _, err := rptSubQuery.ToSql()
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(err, "retention_policies_tosql")
|
|
}
|
|
|
|
query := s.getQueryBuilder().
|
|
Select(`
|
|
RetentionPolicies.Id as "Id",
|
|
RetentionPolicies.DisplayName,
|
|
RetentionPolicies.PostDuration as "PostDuration",
|
|
A.Count AS ChannelCount,
|
|
B.Count AS TeamCount
|
|
`).
|
|
From("RetentionPolicies").
|
|
InnerJoin(`(` + rpcSubQueryString + `) AS A ON RetentionPolicies.Id = A.Id`).
|
|
InnerJoin(`(` + rptSubQueryString + `) AS B ON RetentionPolicies.Id = B.Id`).
|
|
OrderBy("RetentionPolicies.DisplayName, RetentionPolicies.Id")
|
|
|
|
queryString, _, err := query.ToSql()
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(err, "retention_policies_tosql")
|
|
}
|
|
|
|
return queryString, args, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) Get(id string) (*model.RetentionPolicyWithTeamAndChannelCounts, error) {
|
|
queryString, args, err := s.buildGetPolicyQuery(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var policy model.RetentionPolicyWithTeamAndChannelCounts
|
|
if err := s.GetReplica().Get(&policy, queryString, args...); err != nil {
|
|
return nil, err
|
|
}
|
|
return &policy, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetAll(offset, limit int) ([]*model.RetentionPolicyWithTeamAndChannelCounts, error) {
|
|
policies := []*model.RetentionPolicyWithTeamAndChannelCounts{}
|
|
queryString, args, err := s.buildGetPoliciesQuery("", offset, limit)
|
|
if err != nil {
|
|
return policies, err
|
|
}
|
|
err = s.GetReplica().Select(&policies, queryString, args...)
|
|
return policies, err
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetCount() (int64, error) {
|
|
var count int64
|
|
err := s.GetReplica().Get(&count, "SELECT COUNT(*) FROM RetentionPolicies")
|
|
if err != nil {
|
|
return count, err
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) Delete(id string) error {
|
|
query := s.getQueryBuilder().
|
|
Delete("RetentionPolicies").
|
|
Where(sq.Eq{"Id": id})
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return errors.Wrap(err, "retention_policies_tosql")
|
|
}
|
|
|
|
sqlResult, err := s.GetMaster().Exec(queryString, args...)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to permanent delete retention policy with id=%s", id)
|
|
}
|
|
|
|
numRowsAffected, err := sqlResult.RowsAffected()
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to get rows affected")
|
|
} else if numRowsAffected == 0 {
|
|
return errors.New("policy not found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetChannels(policyId string, offset, limit int) (model.ChannelListWithTeamData, error) {
|
|
query := s.getQueryBuilder().
|
|
Select("Teams.DisplayName AS TeamDisplayName", "Teams.Name AS TeamName", "Teams.UpdateAt AS TeamUpdateAt").
|
|
Columns(channelSliceColumns(true, "Channels")...).
|
|
From("RetentionPoliciesChannels").
|
|
InnerJoin("Channels ON RetentionPoliciesChannels.ChannelId = Channels.Id").
|
|
InnerJoin("Teams ON Channels.TeamId = Teams.Id").
|
|
Where(sq.Eq{"RetentionPoliciesChannels.PolicyId": policyId}).
|
|
OrderBy("Channels.DisplayName, Channels.Id").
|
|
Limit(uint64(limit)).
|
|
Offset(uint64(offset))
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "retention_policies_channels_tosql")
|
|
}
|
|
|
|
channels := model.ChannelListWithTeamData{}
|
|
if err := s.GetReplica().Select(&channels, queryString, args...); err != nil {
|
|
return channels, errors.Wrap(err, "failed to find RetentionPoliciesChannels")
|
|
}
|
|
|
|
for _, channel := range channels {
|
|
channel.PolicyID = model.NewPointer(policyId)
|
|
}
|
|
|
|
return channels, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetChannelsCount(policyId string) (int64, error) {
|
|
query := s.getQueryBuilder().
|
|
Select("Count(*)").
|
|
From("RetentionPolicies").
|
|
InnerJoin("RetentionPoliciesChannels ON RetentionPolicies.Id = RetentionPoliciesChannels.PolicyId").
|
|
Where(sq.Eq{"RetentionPolicies.Id": policyId})
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "retention_policies_tosql")
|
|
}
|
|
|
|
var count int64
|
|
if err := s.GetReplica().Get(&count, queryString, args...); err != nil {
|
|
return 0, errors.Wrap(err, "failed to count RetentionPolicies")
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) AddChannels(policyId string, channelIds []string) error {
|
|
if len(channelIds) == 0 {
|
|
return nil
|
|
}
|
|
if err := s.checkChannelsExist(channelIds); err != nil {
|
|
return err
|
|
}
|
|
query := s.getQueryBuilder().
|
|
Insert("RetentionPoliciesChannels").
|
|
Columns("policyId", "channelId")
|
|
|
|
for _, channelId := range channelIds {
|
|
query = query.Values(policyId, channelId)
|
|
}
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return errors.Wrap(err, "retention_policies_channels_tosql")
|
|
}
|
|
|
|
_, err = s.GetMaster().Exec(queryString, args...)
|
|
if err != nil {
|
|
switch dbErr := err.(type) {
|
|
case *pq.Error:
|
|
if dbErr.Code == PGForeignKeyViolationErrorCode {
|
|
return store.NewErrNotFound("RetentionPolicy", policyId)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) RemoveChannels(policyId string, channelIds []string) error {
|
|
if len(channelIds) == 0 {
|
|
return nil
|
|
}
|
|
query := s.getQueryBuilder().
|
|
Delete("RetentionPoliciesChannels").
|
|
Where(sq.And{
|
|
sq.Eq{"PolicyId": policyId},
|
|
sq.Eq{"ChannelId": channelIds},
|
|
})
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return errors.Wrap(err, "retention_policies_channels_tosql")
|
|
}
|
|
|
|
if _, err := s.GetMaster().Exec(queryString, args...); err != nil {
|
|
return errors.Wrapf(err, "failed to permanent delete retention policy channels with policyid=%s", policyId)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetTeams(policyId string, offset, limit int) ([]*model.Team, error) {
|
|
query := s.getQueryBuilder().
|
|
Select(teamSliceColumns()...).
|
|
From("RetentionPoliciesTeams").
|
|
InnerJoin("Teams ON RetentionPoliciesTeams.TeamId = Teams.Id").
|
|
Where(sq.Eq{"RetentionPoliciesTeams.PolicyId": policyId}).
|
|
OrderBy("Teams.DisplayName, Teams.Id").
|
|
Limit(uint64(limit)).
|
|
Offset(uint64(offset))
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "retention_policies_teams_tosql")
|
|
}
|
|
|
|
teams := []*model.Team{}
|
|
if err = s.GetReplica().Select(&teams, queryString, args...); err != nil {
|
|
return teams, errors.Wrap(err, "failed to find Teams")
|
|
}
|
|
|
|
return teams, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetTeamsCount(policyId string) (int64, error) {
|
|
query := s.getQueryBuilder().
|
|
Select("Count(*)").
|
|
From("RetentionPolicies").
|
|
InnerJoin("RetentionPoliciesTeams ON RetentionPolicies.Id = RetentionPoliciesTeams.PolicyId").
|
|
Where(sq.Eq{"RetentionPolicies.Id": policyId})
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "retention_policies_tosql")
|
|
}
|
|
|
|
var count int64
|
|
if err := s.GetReplica().Get(&count, queryString, args...); err != nil {
|
|
return 0, errors.Wrap(err, "failed to count RetentionPolicies")
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) AddTeams(policyId string, teamIds []string) error {
|
|
if len(teamIds) == 0 {
|
|
return nil
|
|
}
|
|
if err := s.checkTeamsExist(teamIds); err != nil {
|
|
return err
|
|
}
|
|
query := s.getQueryBuilder().
|
|
Insert("RetentionPoliciesTeams").
|
|
Columns("PolicyId", "TeamId")
|
|
for _, teamId := range teamIds {
|
|
query = query.Values(policyId, teamId)
|
|
}
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return errors.Wrap(err, "retention_policies_teams_tosql")
|
|
}
|
|
|
|
if _, err := s.GetMaster().Exec(queryString, args...); err != nil {
|
|
return errors.Wrap(err, "failed to insert retention policies teams")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) RemoveTeams(policyId string, teamIds []string) error {
|
|
if len(teamIds) == 0 {
|
|
return nil
|
|
}
|
|
query := s.getQueryBuilder().
|
|
Delete("RetentionPoliciesTeams").
|
|
Where(sq.And{
|
|
sq.Eq{"PolicyId": policyId},
|
|
sq.Eq{"TeamId": teamIds},
|
|
})
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return errors.Wrap(err, "retention_policies_teams_tosql")
|
|
}
|
|
|
|
if _, err := s.GetMaster().Exec(queryString, args...); err != nil {
|
|
return errors.Wrapf(err, "unable to permanent delete retention policies teams with policyid=%s", policyId)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func subQueryIN(property string, query sq.SelectBuilder) sq.Sqlizer {
|
|
queryString, args := query.MustSql()
|
|
|
|
subQuery := fmt.Sprintf("%s IN (%s)", property, queryString)
|
|
return sq.Expr(subQuery, args...)
|
|
}
|
|
|
|
// DeleteOrphanedRows removes entries from RetentionPoliciesChannels and RetentionPoliciesTeams
|
|
// where a channel or team no longer exists.
|
|
func (s *SqlRetentionPolicyStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
|
|
// We need the extra level of nesting to deal with MySQL's locking
|
|
rpcSubQuery := sq.Select("ChannelId").FromSelect(
|
|
sq.Select("ChannelId").
|
|
From("RetentionPoliciesChannels").
|
|
LeftJoin("Channels ON RetentionPoliciesChannels.ChannelId = Channels.Id").
|
|
Where("Channels.Id IS NULL").
|
|
Limit(uint64(limit)),
|
|
"A",
|
|
)
|
|
|
|
rpcDeleteQuery, rpcArgs, err := s.getQueryBuilder().
|
|
Delete("RetentionPoliciesChannels").
|
|
Where(subQueryIN("ChannelId", rpcSubQuery)).
|
|
ToSql()
|
|
if err != nil {
|
|
return int64(0), errors.Wrap(err, "retention_policies_channels_tosql")
|
|
}
|
|
|
|
// We need the extra level of nesting to deal with MySQL's locking
|
|
rptSubQuery := sq.Select("TeamId").FromSelect(
|
|
sq.Select("TeamId").
|
|
From("RetentionPoliciesTeams").
|
|
LeftJoin("Teams ON RetentionPoliciesTeams.TeamId = Teams.Id").
|
|
Where("Teams.Id IS NULL").
|
|
Limit(uint64(limit)),
|
|
"A",
|
|
)
|
|
|
|
rptDeleteQuery, rptArgs, err := s.getQueryBuilder().
|
|
Delete("RetentionPoliciesTeams").
|
|
Where(subQueryIN("TeamId", rptSubQuery)).
|
|
ToSql()
|
|
if err != nil {
|
|
return int64(0), errors.Wrap(err, "retention_policies_teams_tosql")
|
|
}
|
|
|
|
result, err := s.GetMaster().Exec(rpcDeleteQuery, rpcArgs...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
rpcDeleted, err := result.RowsAffected()
|
|
if err != nil {
|
|
return
|
|
}
|
|
result, err = s.GetMaster().Exec(rptDeleteQuery, rptArgs...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
rptDeleted, err := result.RowsAffected()
|
|
if err != nil {
|
|
return
|
|
}
|
|
deleted = rpcDeleted + rptDeleted
|
|
return
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetTeamPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForTeam, error) {
|
|
query := s.getQueryBuilder().
|
|
Select(`Teams.Id AS "Id", RetentionPolicies.PostDuration AS "PostDuration"`).
|
|
From("Users").
|
|
InnerJoin("TeamMembers ON Users.Id = TeamMembers.UserId").
|
|
InnerJoin("Teams ON TeamMembers.TeamId = Teams.Id").
|
|
InnerJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId").
|
|
InnerJoin("RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id").
|
|
Where(
|
|
sq.And{
|
|
sq.Eq{"Users.Id": userID},
|
|
sq.Eq{"TeamMembers.DeleteAt": 0},
|
|
sq.Eq{"Teams.DeleteAt": 0},
|
|
},
|
|
).
|
|
OrderBy("Teams.Id").
|
|
Limit(uint64(limit)).
|
|
Offset(uint64(offset))
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "team_policies_for_user_tosql")
|
|
}
|
|
|
|
policies := []*model.RetentionPolicyForTeam{}
|
|
if err := s.GetReplica().Select(&policies, queryString, args...); err != nil {
|
|
return policies, errors.Wrap(err, "failed to find Users")
|
|
}
|
|
|
|
return policies, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetTeamPoliciesCountForUser(userID string) (int64, error) {
|
|
query := s.getQueryBuilder().
|
|
Select("Count(*)").
|
|
From("Users").
|
|
InnerJoin("TeamMembers ON Users.Id = TeamMembers.UserId").
|
|
InnerJoin("Teams ON TeamMembers.TeamId = Teams.Id").
|
|
InnerJoin("RetentionPoliciesTeams ON Teams.Id = RetentionPoliciesTeams.TeamId").
|
|
InnerJoin("RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id").
|
|
Where(
|
|
sq.And{
|
|
sq.Eq{"Users.Id": userID},
|
|
sq.Eq{"TeamMembers.DeleteAt": 0},
|
|
sq.Eq{"Teams.DeleteAt": 0},
|
|
},
|
|
)
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "team_policies_count_for_user_tosql")
|
|
}
|
|
|
|
var count int64
|
|
if err := s.GetReplica().Get(&count, queryString, args...); err != nil {
|
|
return 0, errors.Wrap(err, "failed to count TeamPoliciesCountForUser")
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetChannelPoliciesForUser(userID string, offset, limit int) ([]*model.RetentionPolicyForChannel, error) {
|
|
query := s.getQueryBuilder().
|
|
Select(`Channels.Id as "Id", RetentionPolicies.PostDuration as "PostDuration"`).
|
|
From("Users").
|
|
InnerJoin("ChannelMembers ON Users.Id = ChannelMembers.UserId").
|
|
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
|
|
InnerJoin("RetentionPoliciesChannels ON Channels.Id = RetentionPoliciesChannels.ChannelId").
|
|
InnerJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
|
|
Where(
|
|
sq.And{
|
|
sq.Eq{"Users.Id": userID},
|
|
sq.Eq{"Channels.DeleteAt": 0},
|
|
},
|
|
).
|
|
OrderBy("Channels.Id").
|
|
Limit(uint64(limit)).
|
|
Offset(uint64(offset))
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "channel_policies_for_user_tosql")
|
|
}
|
|
|
|
policies := []*model.RetentionPolicyForChannel{}
|
|
if err := s.GetReplica().Select(&policies, queryString, args...); err != nil {
|
|
return nil, errors.Wrap(err, "failed to find Users")
|
|
}
|
|
|
|
return policies, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetChannelPoliciesCountForUser(userID string) (int64, error) {
|
|
query := s.getQueryBuilder().
|
|
Select("Count(*)").
|
|
From("Users").
|
|
InnerJoin("ChannelMembers ON Users.Id = ChannelMembers.UserId").
|
|
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
|
|
InnerJoin("RetentionPoliciesChannels ON Channels.Id = RetentionPoliciesChannels.ChannelId").
|
|
InnerJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
|
|
Where(
|
|
sq.And{
|
|
sq.Eq{"Users.Id": userID},
|
|
sq.Eq{"Channels.DeleteAt": 0},
|
|
},
|
|
)
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "channel_policies_count_users_tosql")
|
|
}
|
|
|
|
var count int64
|
|
if err := s.GetReplica().Get(&count, queryString, args...); err != nil {
|
|
return 0, errors.Wrap(err, "failed to count ChannelPoliciesCountForUser")
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func scanRetentionIdsForDeletion(rows *sql.Rows, isPostgres bool) ([]*model.RetentionIdsForDeletion, error) {
|
|
idsForDeletion := []*model.RetentionIdsForDeletion{}
|
|
for rows.Next() {
|
|
var row model.RetentionIdsForDeletion
|
|
if isPostgres {
|
|
if err := rows.Scan(
|
|
&row.Id, &row.TableName, pq.Array(&row.Ids),
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "unable to scan columns")
|
|
}
|
|
} else {
|
|
var ids []byte
|
|
if err := rows.Scan(
|
|
&row.Id, &row.TableName, &ids,
|
|
); err != nil {
|
|
return nil, errors.Wrap(err, "unable to scan columns")
|
|
}
|
|
if err := json.Unmarshal(ids, &row.Ids); err != nil {
|
|
return nil, errors.Wrap(err, "failed to unmarshal ids")
|
|
}
|
|
}
|
|
|
|
idsForDeletion = append(idsForDeletion, &row)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, errors.Wrap(err, "error while iterating over rows")
|
|
}
|
|
return idsForDeletion, nil
|
|
}
|
|
|
|
func (s *SqlRetentionPolicyStore) GetIdsForDeletionByTableName(tableName string, limit int) ([]*model.RetentionIdsForDeletion, error) {
|
|
query := s.getQueryBuilder().
|
|
Select("Id", "TableName", "Ids").
|
|
From("RetentionIdsForDeletion").
|
|
Where(
|
|
sq.Eq{"TableName": tableName},
|
|
).
|
|
Limit(uint64(limit))
|
|
|
|
queryString, args, err := query.ToSql()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get_ids_for_deletion_tosql")
|
|
}
|
|
|
|
rows, err := s.GetReplica().DB.Query(queryString, args...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get ids for deletion")
|
|
}
|
|
defer rows.Close()
|
|
|
|
isPostgres := s.DriverName() == model.DatabaseDriverPostgres
|
|
idsForDeletion, err := scanRetentionIdsForDeletion(rows, isPostgres)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to scan ids for deletion")
|
|
}
|
|
|
|
return idsForDeletion, nil
|
|
}
|
|
|
|
func insertRetentionIdsForDeletion(txn *sqlxTxWrapper, row *model.RetentionIdsForDeletion, s *SqlStore) error {
|
|
row.PreSave()
|
|
insertBuilder := s.getQueryBuilder().
|
|
Insert("RetentionIdsForDeletion").
|
|
Columns("Id", "TableName", "Ids")
|
|
if s.DriverName() == model.DatabaseDriverPostgres {
|
|
insertBuilder = insertBuilder.
|
|
Values(row.Id, row.TableName, pq.Array(row.Ids))
|
|
} else {
|
|
jsonIds, err := json.Marshal(row.Ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
insertBuilder = insertBuilder.
|
|
Values(row.Id, row.TableName, jsonIds)
|
|
}
|
|
insertQuery, insertArgs, err := insertBuilder.ToSql()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = txn.Exec(insertQuery, insertArgs...); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RetentionPolicyBatchDeletionInfo gives information on how to delete records
|
|
// under a retention policy; see `genericPermanentDeleteBatchForRetentionPolicies`.
|
|
//
|
|
// `BaseBuilder` should already have selected the primary key(s) for the main table
|
|
// and should be joined to a table with a ChannelId column, which will be used to join
|
|
// on the Channels table.
|
|
// `Table` is the name of the table from which records are being deleted.
|
|
// `TimeColumn` is the name of the column which contains the timestamp of the record.
|
|
// `PrimaryKeys` contains the primary keys of `table`. It should be the same as the
|
|
// `From` clause in `baseBuilder`.
|
|
// `ChannelIDTable` is the table which contains the ChannelId column, it may be the
|
|
// same as `table`, or will be different if a join was used.
|
|
// `NowMillis` must be a Unix timestamp in milliseconds and is used by the granular
|
|
// policies; if `nowMillis - timestamp(record)` is greater than
|
|
// the post duration of a granular policy, than the record will be deleted.
|
|
// `GlobalPolicyEndTime` is used by the global policy; any record older than this time
|
|
// will be deleted by the global policy if it does not fall under a granular policy.
|
|
// To disable the granular policies, set `NowMillis` to 0.
|
|
// To disable the global policy, set `GlobalPolicyEndTime` to 0.
|
|
type RetentionPolicyBatchDeletionInfo struct {
|
|
BaseBuilder sq.SelectBuilder
|
|
Table string
|
|
TimeColumn string
|
|
PrimaryKeys []string
|
|
ChannelIDTable string
|
|
NowMillis int64
|
|
GlobalPolicyEndTime int64
|
|
Limit int64
|
|
StoreDeletedIds bool
|
|
}
|
|
|
|
// genericPermanentDeleteBatchForRetentionPolicies is a helper function for tables
|
|
// which need to delete records for granular and global policies.
|
|
func genericPermanentDeleteBatchForRetentionPolicies(
|
|
r RetentionPolicyBatchDeletionInfo,
|
|
s *SqlStore,
|
|
cursor model.RetentionPolicyCursor,
|
|
) (int64, model.RetentionPolicyCursor, error) {
|
|
baseBuilder := r.BaseBuilder.InnerJoin("Channels ON " + r.ChannelIDTable + ".ChannelId = Channels.Id")
|
|
|
|
scopedTimeColumn := r.Table + "." + r.TimeColumn
|
|
nowStr := strconv.FormatInt(r.NowMillis, 10)
|
|
// A record falls under the scope of a granular retention policy if:
|
|
// 1. The policy's post duration is >= 0
|
|
// 2. The record's lifespan has not exceeded the policy's post duration
|
|
const millisecondsInADay = 24 * 60 * 60 * 1000
|
|
fallsUnderGranularPolicy := sq.And{
|
|
sq.GtOrEq{"RetentionPolicies.PostDuration": 0},
|
|
sq.Expr(nowStr + " - " + scopedTimeColumn + " > RetentionPolicies.PostDuration * " + strconv.FormatInt(millisecondsInADay, 10)),
|
|
}
|
|
|
|
// If the caller wants to disable the global policy from running
|
|
if r.GlobalPolicyEndTime <= 0 {
|
|
cursor.GlobalPoliciesDone = true
|
|
}
|
|
// If the caller wants to disable the granular policies from running
|
|
if r.NowMillis <= 0 {
|
|
cursor.ChannelPoliciesDone = true
|
|
cursor.TeamPoliciesDone = true
|
|
}
|
|
|
|
var totalRowsAffected int64
|
|
|
|
// First, delete all of the records which fall under the scope of a channel-specific policy
|
|
if !cursor.ChannelPoliciesDone {
|
|
channelPoliciesBuilder := baseBuilder.
|
|
InnerJoin("RetentionPoliciesChannels ON " + r.ChannelIDTable + ".ChannelId = RetentionPoliciesChannels.ChannelId").
|
|
InnerJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
|
|
Where(fallsUnderGranularPolicy).
|
|
Limit(uint64(r.Limit))
|
|
rowsAffected, err := genericRetentionPoliciesDeletion(channelPoliciesBuilder, r, s)
|
|
if err != nil {
|
|
return 0, cursor, err
|
|
}
|
|
if rowsAffected < r.Limit {
|
|
cursor.ChannelPoliciesDone = true
|
|
}
|
|
totalRowsAffected += rowsAffected
|
|
r.Limit -= rowsAffected
|
|
}
|
|
|
|
// Next, delete all of the records which fall under the scope of a team-specific policy
|
|
if cursor.ChannelPoliciesDone && !cursor.TeamPoliciesDone {
|
|
// Channel-specific policies override team-specific policies.
|
|
teamPoliciesBuilder := baseBuilder.
|
|
LeftJoin("RetentionPoliciesChannels ON " + r.ChannelIDTable + ".ChannelId = RetentionPoliciesChannels.ChannelId").
|
|
InnerJoin("RetentionPoliciesTeams ON Channels.TeamId = RetentionPoliciesTeams.TeamId").
|
|
InnerJoin("RetentionPolicies ON RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id").
|
|
Where(sq.And{
|
|
sq.Eq{"RetentionPoliciesChannels.PolicyId": nil},
|
|
sq.Expr("RetentionPoliciesTeams.PolicyId = RetentionPolicies.Id"),
|
|
}).
|
|
Where(fallsUnderGranularPolicy).
|
|
Limit(uint64(r.Limit))
|
|
rowsAffected, err := genericRetentionPoliciesDeletion(teamPoliciesBuilder, r, s)
|
|
if err != nil {
|
|
return 0, cursor, err
|
|
}
|
|
if rowsAffected < r.Limit {
|
|
cursor.TeamPoliciesDone = true
|
|
}
|
|
totalRowsAffected += rowsAffected
|
|
r.Limit -= rowsAffected
|
|
}
|
|
|
|
// Finally, delete all of the records which fall under the scope of the global policy
|
|
if cursor.ChannelPoliciesDone && cursor.TeamPoliciesDone && !cursor.GlobalPoliciesDone {
|
|
// Granular policies override the global policy.
|
|
globalPolicyBuilder := baseBuilder.
|
|
LeftJoin("RetentionPoliciesChannels ON " + r.ChannelIDTable + ".ChannelId = RetentionPoliciesChannels.ChannelId").
|
|
LeftJoin("RetentionPoliciesTeams ON Channels.TeamId = RetentionPoliciesTeams.TeamId").
|
|
LeftJoin("RetentionPolicies ON RetentionPoliciesChannels.PolicyId = RetentionPolicies.Id").
|
|
Where(sq.And{
|
|
sq.Eq{"RetentionPoliciesChannels.PolicyId": nil},
|
|
sq.Eq{"RetentionPoliciesTeams.PolicyId": nil},
|
|
}).
|
|
Where(sq.Lt{scopedTimeColumn: r.GlobalPolicyEndTime}).
|
|
Limit(uint64(r.Limit))
|
|
rowsAffected, err := genericRetentionPoliciesDeletion(globalPolicyBuilder, r, s)
|
|
if err != nil {
|
|
return 0, cursor, err
|
|
}
|
|
if rowsAffected < r.Limit {
|
|
cursor.GlobalPoliciesDone = true
|
|
}
|
|
totalRowsAffected += rowsAffected
|
|
}
|
|
|
|
return totalRowsAffected, cursor, nil
|
|
}
|
|
|
|
// genericRetentionPoliciesDeletion actually executes the DELETE query using a sq.SelectBuilder
|
|
// which selects the rows to delete.
|
|
func genericRetentionPoliciesDeletion(
|
|
builder sq.SelectBuilder,
|
|
r RetentionPolicyBatchDeletionInfo,
|
|
s *SqlStore,
|
|
) (rowsAffected int64, err error) {
|
|
query, args, err := builder.ToSql()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, r.Table+"_tosql")
|
|
}
|
|
|
|
if r.StoreDeletedIds {
|
|
txn, err := s.GetMaster().Beginx()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer finalizeTransactionX(txn, &err)
|
|
|
|
primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")"
|
|
|
|
query = fmt.Sprintf("DELETE FROM %s WHERE %s IN (%s) RETURNING %s.%s", r.Table, primaryKeysStr, query, r.Table, r.PrimaryKeys[0])
|
|
var rows *sql.Rows
|
|
rows, err = txn.Query(query, args...)
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "failed to delete "+r.Table)
|
|
}
|
|
|
|
defer rows.Close()
|
|
ids := []string{}
|
|
for rows.Next() {
|
|
var id string
|
|
if err = rows.Scan(&id); err != nil {
|
|
return 0, errors.Wrap(err, "unable to scan from rows")
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
if err = rows.Err(); err != nil {
|
|
return 0, errors.Wrap(err, "failed while iterating over rows")
|
|
}
|
|
rowsAffected = int64(len(ids))
|
|
|
|
if len(ids) > 0 {
|
|
retentionIdsRow := model.RetentionIdsForDeletion{
|
|
TableName: r.Table,
|
|
Ids: ids,
|
|
}
|
|
err = insertRetentionIdsForDeletion(txn, &retentionIdsRow, s)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
if err = txn.Commit(); err != nil {
|
|
return 0, err
|
|
}
|
|
} else {
|
|
primaryKeysStr := "(" + strings.Join(r.PrimaryKeys, ",") + ")"
|
|
query = fmt.Sprintf("DELETE FROM %s WHERE %s IN (%s)", r.Table, primaryKeysStr, query)
|
|
result, err := s.GetMaster().Exec(query, args...)
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "failed to delete "+r.Table)
|
|
}
|
|
rowsAffected, err = result.RowsAffected()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "failed to get rows affected for "+r.Table)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func deleteFromRetentionIdsTx(txn *sqlxTxWrapper, id string) error {
|
|
_, err := txn.Exec("DELETE FROM RetentionIdsForDeletion WHERE Id = ?", id)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to delete from RetentionIdsForDeletion")
|
|
}
|
|
|
|
return nil
|
|
}
|