mattermost-community-enterp.../channels/store/sqlstore/reaction_store.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
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>
2025-12-17 23:59:07 +09:00

430 lines
12 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"fmt"
"time"
sq "github.com/mattermost/squirrel"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/pkg/errors"
)
type SqlReactionStore struct {
*SqlStore
}
func newSqlReactionStore(sqlStore *SqlStore) store.ReactionStore {
return &SqlReactionStore{sqlStore}
}
func (s *SqlReactionStore) Save(reaction *model.Reaction) (re *model.Reaction, err error) {
reaction.PreSave()
if err := reaction.IsValid(); err != nil {
return nil, err
}
transaction, err := s.GetMaster().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if reaction.ChannelId == "" {
// get channelId, if not already populated
var channelIds []string
query := "SELECT ChannelId from Posts where Id = ?"
err = transaction.Select(&channelIds, query, reaction.PostId)
if err != nil {
return nil, errors.Wrap(err, "failed while getting channelId from Posts")
}
if len(channelIds) == 0 {
return nil, store.NewErrNotFound("Post", reaction.PostId)
}
reaction.ChannelId = channelIds[0]
}
err = s.saveReactionAndUpdatePost(transaction, reaction)
if err != nil {
// We don't consider duplicated save calls as an error
if !IsUniqueConstraintError(err, []string{"reactions_pkey", "PRIMARY"}) {
return nil, errors.Wrap(err, "failed while saving reaction or updating post")
}
} else {
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
}
return reaction, nil
}
func (s *SqlReactionStore) Delete(reaction *model.Reaction) (re *model.Reaction, err error) {
reaction.PreUpdate()
transaction, err := s.GetMaster().Beginx()
if err != nil {
return nil, errors.Wrap(err, "begin_transaction")
}
defer finalizeTransactionX(transaction, &err)
if err := deleteReactionAndUpdatePost(transaction, reaction); err != nil {
return nil, errors.Wrap(err, "deleteReactionAndUpdatePost")
}
if err := transaction.Commit(); err != nil {
return nil, errors.Wrap(err, "commit_transaction")
}
return reaction, nil
}
// GetForPost returns all reactions associated with `postId` that are not deleted.
func (s *SqlReactionStore) GetForPost(postId string, allowFromCache bool) ([]*model.Reaction, error) {
builder := s.getQueryBuilder().
Select("UserId", "PostId", "EmojiName", "CreateAt", "COALESCE(UpdateAt, CreateAt) As UpdateAt",
"COALESCE(DeleteAt, 0) As DeleteAt", "RemoteId", "ChannelId").
From("Reactions").
Where(sq.Eq{"PostId": postId}).
Where(sq.Eq{"COALESCE(DeleteAt, 0)": 0}).
OrderBy("CreateAt")
var reactions []*model.Reaction
if err := s.GetReplica().SelectBuilder(&reactions, builder); err != nil {
return nil, errors.Wrapf(err, "failed to get Reactions with postId=%s", postId)
}
return reactions, nil
}
func (s *SqlReactionStore) ExistsOnPost(postId string, emojiName string) (bool, error) {
query := s.getQueryBuilder().
Select("1").
From("Reactions").
Where(sq.Eq{"PostId": postId}).
Where(sq.Eq{"EmojiName": emojiName}).
Where(sq.Eq{"COALESCE(DeleteAt, 0)": 0})
var hasRows bool
if err := s.GetReplica().GetBuilder(&hasRows, query); err != nil {
if err == sql.ErrNoRows {
return false, nil
}
return false, errors.Wrap(err, "failed to check for existing reaction")
}
return hasRows, nil
}
// GetForPostSince returns all reactions associated with `postId` updated after `since`.
func (s *SqlReactionStore) GetForPostSince(postId string, since int64, excludeRemoteId string, inclDeleted bool) ([]*model.Reaction, error) {
query := s.getQueryBuilder().
Select("UserId", "PostId", "EmojiName", "CreateAt", "COALESCE(UpdateAt, CreateAt) As UpdateAt",
"COALESCE(DeleteAt, 0) As DeleteAt", "RemoteId").
From("Reactions").
Where(sq.Eq{"PostId": postId}).
Where(sq.Gt{"UpdateAt": since}).
OrderBy("CreateAt")
if excludeRemoteId != "" {
query = query.Where(sq.NotEq{"COALESCE(RemoteId, '')": excludeRemoteId})
}
if !inclDeleted {
query = query.Where(sq.Eq{"COALESCE(DeleteAt, 0)": 0})
}
var reactions []*model.Reaction
if err := s.GetReplica().SelectBuilder(&reactions, query); err != nil {
return nil, errors.Wrapf(err, "failed to find reactions")
}
return reactions, nil
}
func (s *SqlReactionStore) GetUniqueCountForPost(postId string) (int, error) {
query := s.getQueryBuilder().
Select("COUNT(DISTINCT EmojiName)").
From("Reactions").
Where(sq.Eq{"PostId": postId}).
Where(sq.Eq{"DeleteAt": 0})
var count int64
err := s.GetReplica().GetBuilder(&count, query)
if err != nil {
return 0, errors.Wrap(err, "failed to count Reactions")
}
return int(count), nil
}
func (s *SqlReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
placeholder, values := constructArrayArgs(postIds)
var reactions []*model.Reaction
if err := s.GetReplica().Select(&reactions,
`SELECT
UserId,
PostId,
EmojiName,
CreateAt,
COALESCE(UpdateAt, CreateAt) As UpdateAt,
COALESCE(DeleteAt, 0) As DeleteAt,
RemoteId,
ChannelId
FROM
Reactions
WHERE
PostId IN `+placeholder+` AND COALESCE(DeleteAt, 0) = 0
ORDER BY
CreateAt`, values...); err != nil {
return nil, errors.Wrap(err, "failed to get Reactions")
}
return reactions, nil
}
func (s *SqlReactionStore) GetSingle(userID, postID, remoteID, emojiName string) (*model.Reaction, error) {
query := s.getQueryBuilder().
Select("UserId", "PostId", "EmojiName", "CreateAt",
"COALESCE(UpdateAt, CreateAt) As UpdateAt", "COALESCE(DeleteAt, 0) As DeleteAt",
"RemoteId", "ChannelId").
From("Reactions").
Where(sq.Eq{"UserId": userID}).
Where(sq.Eq{"PostId": postID}).
Where(sq.Eq{"COALESCE(RemoteId, '')": remoteID}).
Where(sq.Eq{"EmojiName": emojiName})
var reactions []*model.Reaction
if err := s.GetReplica().SelectBuilder(&reactions, query); err != nil {
return nil, errors.Wrapf(err, "failed to find reaction")
}
if len(reactions) == 0 {
return nil, store.NewErrNotFound("Reaction", fmt.Sprintf("user_id=%s, post_id=%s, remote_id=%s, emoji_name=%s",
userID, postID, remoteID, emojiName))
}
return reactions[0], nil
}
func (s *SqlReactionStore) DeleteAllWithEmojiName(emojiName string) error {
var reactions []*model.Reaction
now := model.GetMillis()
if err := s.GetReplica().Select(&reactions,
`SELECT
UserId,
PostId,
EmojiName,
CreateAt,
COALESCE(UpdateAt, CreateAt) As UpdateAt,
COALESCE(DeleteAt, 0) As DeleteAt,
RemoteId
FROM
Reactions
WHERE
EmojiName = ? AND COALESCE(DeleteAt, 0) = 0`, emojiName); err != nil {
return errors.Wrapf(err, "failed to get Reactions with emojiName=%s", emojiName)
}
_, err := s.GetMaster().Exec(
`UPDATE
Reactions
SET
UpdateAt = ?, DeleteAt = ?
WHERE
EmojiName = ? AND COALESCE(DeleteAt, 0) = 0`, now, now, emojiName)
if err != nil {
return errors.Wrapf(err, "failed to delete Reactions with emojiName=%s", emojiName)
}
for _, reaction := range reactions {
_, err := s.GetMaster().Exec(UpdatePostHasReactionsOnDeleteQuery, now, reaction.PostId, reaction.PostId)
if err != nil {
mlog.Warn("Unable to update Post.HasReactions while removing reactions",
mlog.String("post_id", reaction.PostId),
mlog.Err(err))
}
}
return nil
}
func (s *SqlReactionStore) permanentDeleteReactions(userId string) ([]string, error) {
txn, err := s.GetMaster().Beginx()
if err != nil {
return nil, err
}
defer finalizeTransactionX(txn, &err)
postIds := []string{}
err = txn.Select(&postIds, "SELECT PostId FROM Reactions WHERE UserId = ?", userId)
if err != nil {
return nil, errors.Wrapf(err, "failed to get Reactions with userId=%s", userId)
}
query := s.getQueryBuilder().
Delete("Reactions").
Where(sq.And{
sq.Eq{"PostId": postIds},
sq.Eq{"UserId": userId},
})
_, err = txn.ExecBuilder(query)
if err != nil {
return nil, errors.Wrapf(err, "failed to delete reactions with userId=%s", userId)
}
if err = txn.Commit(); err != nil {
return nil, err
}
return postIds, nil
}
func (s SqlReactionStore) PermanentDeleteByUser(userId string) error {
now := model.GetMillis()
postIds, err := s.permanentDeleteReactions(userId)
if err != nil {
return err
}
transaction, err := s.GetMaster().Beginx()
if err != nil {
return err
}
defer finalizeTransactionX(transaction, &err)
for _, postId := range postIds {
_, err = transaction.Exec(UpdatePostHasReactionsOnDeleteQuery, now, postId, postId)
if err != nil {
mlog.Warn("Unable to update Post.HasReactions while removing reactions",
mlog.String("post_id", postId),
mlog.Err(err))
}
time.Sleep(10 * time.Millisecond)
}
if err = transaction.Commit(); err != nil {
return err
}
return nil
}
func (s *SqlReactionStore) DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) (int64, error) {
txn, err := s.GetMaster().Beginx()
if err != nil {
return 0, err
}
defer finalizeTransactionX(txn, &err)
query := s.getQueryBuilder().
Delete("Reactions").
Where(
sq.Eq{"PostId": r.Ids},
)
sqlResult, err := txn.ExecBuilder(query)
if err != nil {
return 0, errors.Wrapf(err, "failed to delete orphaned reactions with RetentionIdsForDeletion Id=%s", r.Id)
}
err = deleteFromRetentionIdsTx(txn, r.Id)
if err != nil {
return 0, err
}
if err = txn.Commit(); err != nil {
return 0, err
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "unable to retrieve rows affected")
}
return rowsAffected, nil
}
func (s *SqlReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
var query string
if s.DriverName() == "postgres" {
query = "DELETE from Reactions WHERE CreateAt = any (array (SELECT CreateAt FROM Reactions WHERE CreateAt < ? LIMIT ?))"
} else {
query = "DELETE from Reactions WHERE CreateAt < ? LIMIT ?"
}
sqlResult, err := s.GetMaster().Exec(query, endTime, limit)
if err != nil {
return 0, errors.Wrap(err, "failed to delete Reactions")
}
rowsAffected, err := sqlResult.RowsAffected()
if err != nil {
return 0, errors.Wrap(err, "unable to get rows affected for deleted Reactions")
}
return rowsAffected, nil
}
func (s *SqlReactionStore) saveReactionAndUpdatePost(transaction *sqlxTxWrapper, reaction *model.Reaction) error {
reaction.DeleteAt = 0
if _, err := transaction.NamedExec(
`INSERT INTO
Reactions
(UserId, PostId, EmojiName, CreateAt, UpdateAt, DeleteAt, RemoteId, ChannelId)
VALUES
(:UserId, :PostId, :EmojiName, :CreateAt, :UpdateAt, :DeleteAt, :RemoteId, :ChannelId)
ON CONFLICT (UserId, PostId, EmojiName)
DO UPDATE SET UpdateAt = :UpdateAt, DeleteAt = :DeleteAt, RemoteId = :RemoteId, ChannelId = :ChannelId`, reaction); err != nil {
return err
}
return updatePostForReactionsOnInsert(transaction, reaction.PostId)
}
func deleteReactionAndUpdatePost(transaction *sqlxTxWrapper, reaction *model.Reaction) error {
if _, err := transaction.Exec(
`UPDATE
Reactions
SET
UpdateAt = ?, DeleteAt = ?, RemoteId = ?
WHERE
PostId = ? AND
UserId = ? AND
EmojiName = ?`, reaction.UpdateAt, reaction.UpdateAt, reaction.RemoteId, reaction.PostId, reaction.UserId, reaction.EmojiName); err != nil {
return err
}
return updatePostForReactionsOnDelete(transaction, reaction.PostId)
}
const (
UpdatePostHasReactionsOnDeleteQuery = `UPDATE
Posts
SET
UpdateAt = ?,
HasReactions = (SELECT count(0) > 0 FROM Reactions WHERE PostId = ? AND COALESCE(DeleteAt, 0) = 0)
WHERE
Id = ?`
)
func updatePostForReactionsOnDelete(transaction *sqlxTxWrapper, postId string) error {
updateAt := model.GetMillis()
_, err := transaction.Exec(UpdatePostHasReactionsOnDeleteQuery, updateAt, postId, postId)
return err
}
func updatePostForReactionsOnInsert(transaction *sqlxTxWrapper, postId string) error {
_, err := transaction.Exec(
`UPDATE
Posts
SET
HasReactions = True,
UpdateAt = ?
WHERE
Id = ?`,
model.GetMillis(),
postId,
)
return err
}