feat(messages): sync saved message tags
This commit is contained in:
parent
a785ae7491
commit
6b3eba6c5d
42 changed files with 1581 additions and 551 deletions
|
|
@ -121,70 +121,3 @@ func (s *ChannelStore) ClearRecentMessageReactions(ctx context.Context, userID i
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ListSavedReactionTags(ctx context.Context, userID int64, limit int) ([]domain.SavedReactionTag, error) {
|
||||
if userID == 0 {
|
||||
return nil, domain.ErrChannelInvalid
|
||||
}
|
||||
if limit <= 0 {
|
||||
return []domain.SavedReactionTag{}, nil
|
||||
}
|
||||
if limit > domain.MaxSavedReactionTags {
|
||||
limit = domain.MaxSavedReactionTags
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT reaction_type, reaction_value, title, reaction_count
|
||||
FROM user_saved_reaction_tags
|
||||
WHERE user_id = $1
|
||||
ORDER BY reaction_count DESC, updated_at DESC, reaction_type ASC, reaction_value ASC
|
||||
LIMIT $2`, userID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list saved reaction tags: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]domain.SavedReactionTag, 0, limit)
|
||||
for rows.Next() {
|
||||
var reactionType, reactionValue, title string
|
||||
var count int
|
||||
if err := rows.Scan(&reactionType, &reactionValue, &title, &count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reaction, ok := domain.MessageReactionFromValue(domain.MessageReactionType(reactionType), reactionValue)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, domain.SavedReactionTag{
|
||||
UserID: userID,
|
||||
Reaction: reaction,
|
||||
Title: title,
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) UpsertSavedReactionTag(ctx context.Context, tag domain.SavedReactionTag) error {
|
||||
if tag.UserID == 0 || tag.Reaction.Type != domain.MessageReactionEmoji {
|
||||
return domain.ErrChannelInvalid
|
||||
}
|
||||
reactionValue := strings.TrimSpace(tag.Reaction.Emoticon)
|
||||
if reactionValue == "" {
|
||||
return domain.ErrChannelInvalid
|
||||
}
|
||||
if tag.Count < 0 {
|
||||
tag.Count = 0
|
||||
}
|
||||
if _, err := s.db.Exec(ctx, `
|
||||
INSERT INTO user_saved_reaction_tags (user_id, reaction_type, reaction_value, title, reaction_count)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (user_id, reaction_type, reaction_value) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
reaction_count = GREATEST(user_saved_reaction_tags.reaction_count, EXCLUDED.reaction_count),
|
||||
updated_at = now()`, tag.UserID, string(tag.Reaction.Type), reactionValue, tag.Title, tag.Count); err != nil {
|
||||
return fmt.Errorf("upsert saved reaction tag: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *MessageStore) GetByIDs(ctx context.Context, userID int64, ids []int) (domain.MessageList, error) {
|
||||
|
|
@ -92,6 +94,7 @@ func (s *MessageStore) ListByUser(ctx context.Context, userID int64, filter doma
|
|||
savedPeerType = string(filter.SavedPeer.Type)
|
||||
savedPeerID = filter.SavedPeer.ID
|
||||
}
|
||||
savedReactionKeys := postgresSavedReactionKeys(filter.SavedReactions)
|
||||
// add_offset>=0 是 backward 热路径(初始加载/上滑翻页,占 getHistory 绝大多数)。
|
||||
// 走扁平静态查询 ListMessagesBackward:规划仅单 index scan + 2 LEFT JOIN,避免
|
||||
// ListMessagesByUser 大 CTE 把 4 个分支+total 全树规划(6.7ms→~1ms)。与 CTE
|
||||
|
|
@ -100,23 +103,26 @@ func (s *MessageStore) ListByUser(ctx context.Context, userID int64, filter doma
|
|||
var rows []sqlcgen.ListMessagesByUserRow
|
||||
if addOffset >= 0 {
|
||||
bw, err := s.q.ListMessagesBackward(ctx, sqlcgen.ListMessagesBackwardParams{
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
RestrictPeerIds: filter.RestrictPeerIDs,
|
||||
PeerIds: filter.PeerIDs,
|
||||
Query: filter.Query,
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
OffsetDate: pgInt32NonNegative(filter.OffsetDate),
|
||||
OffsetID: pgInt32NonNegative(filter.OffsetID),
|
||||
RowOffset: pgInt32Bounded(addOffset),
|
||||
LimitCount: int32(queryLimit),
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
RestrictPeerIds: filter.RestrictPeerIDs,
|
||||
PeerIds: filter.PeerIDs,
|
||||
Query: filter.Query,
|
||||
MinDate: pgInt32NonNegative(filter.MinDate),
|
||||
MaxDate: pgInt32NonNegative(filter.MaxDate),
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
SavedReactionKeys: savedReactionKeys,
|
||||
OffsetDate: pgInt32NonNegative(filter.OffsetDate),
|
||||
OffsetID: pgInt32NonNegative(filter.OffsetID),
|
||||
RowOffset: pgInt32Bounded(addOffset),
|
||||
LimitCount: int32(queryLimit),
|
||||
})
|
||||
if err != nil {
|
||||
return domain.MessageList{}, fmt.Errorf("list messages (backward): %w", err)
|
||||
|
|
@ -127,19 +133,22 @@ func (s *MessageStore) ListByUser(ctx context.Context, userID int64, filter doma
|
|||
}
|
||||
if filter.NeedTotalCount {
|
||||
total, err := s.q.CountMessagesByUser(ctx, sqlcgen.CountMessagesByUserParams{
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
RestrictPeerIds: filter.RestrictPeerIDs,
|
||||
PeerIds: filter.PeerIDs,
|
||||
Query: filter.Query,
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
RestrictPeerIds: filter.RestrictPeerIDs,
|
||||
PeerIds: filter.PeerIDs,
|
||||
Query: filter.Query,
|
||||
MinDate: pgInt32NonNegative(filter.MinDate),
|
||||
MaxDate: pgInt32NonNegative(filter.MaxDate),
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
SavedReactionKeys: savedReactionKeys,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.MessageList{}, fmt.Errorf("count messages: %w", err)
|
||||
|
|
@ -153,24 +162,27 @@ func (s *MessageStore) ListByUser(ctx context.Context, userID int64, filter doma
|
|||
} else {
|
||||
var err error
|
||||
rows, err = s.q.ListMessagesByUser(ctx, sqlcgen.ListMessagesByUserParams{
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
RestrictPeerIds: filter.RestrictPeerIDs,
|
||||
PeerIds: filter.PeerIDs,
|
||||
Query: filter.Query,
|
||||
OffsetID: pgInt32NonNegative(filter.OffsetID),
|
||||
OffsetDate: pgInt32NonNegative(filter.OffsetDate),
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
AddOffset: pgInt32Bounded(addOffset),
|
||||
LimitCount: int32(queryLimit),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
NeedTotalCount: filter.NeedTotalCount,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
OwnerUserID: userID,
|
||||
HasPeer: filter.HasPeer,
|
||||
PeerType: string(filter.Peer.Type),
|
||||
PeerID: filter.Peer.ID,
|
||||
RestrictPeerIds: filter.RestrictPeerIDs,
|
||||
PeerIds: filter.PeerIDs,
|
||||
Query: filter.Query,
|
||||
MinDate: pgInt32NonNegative(filter.MinDate),
|
||||
MaxDate: pgInt32NonNegative(filter.MaxDate),
|
||||
OffsetID: pgInt32NonNegative(filter.OffsetID),
|
||||
OffsetDate: pgInt32NonNegative(filter.OffsetDate),
|
||||
MaxID: pgInt32NonNegative(filter.MaxID),
|
||||
MinID: pgInt32NonNegative(filter.MinID),
|
||||
AddOffset: pgInt32Bounded(addOffset),
|
||||
LimitCount: int32(queryLimit),
|
||||
PinnedOnly: filter.PinnedOnly,
|
||||
MusicOnly: filter.MusicOnly,
|
||||
NeedTotalCount: filter.NeedTotalCount,
|
||||
SavedPeerType: savedPeerType,
|
||||
SavedPeerID: savedPeerID,
|
||||
SavedReactionKeys: savedReactionKeys,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.MessageList{}, fmt.Errorf("list messages: %w", err)
|
||||
|
|
@ -273,6 +285,16 @@ func (s *MessageStore) ListByUser(ctx context.Context, userID int64, filter doma
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func postgresSavedReactionKeys(reactions []domain.MessageReaction) []string {
|
||||
out := make([]string, 0, len(reactions))
|
||||
for _, reaction := range reactions {
|
||||
if reaction.Valid() {
|
||||
out = append(out, string(reaction.Type)+":"+reaction.Value())
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *MessageStore) ReadHistory(ctx context.Context, req domain.ReadHistoryRequest) (res domain.ReadHistoryResult, err error) {
|
||||
res = domain.ReadHistoryResult{OwnerUserID: req.OwnerUserID, Peer: req.Peer, MaxID: req.MaxID}
|
||||
if req.OwnerUserID == 0 {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ func (s *MessageStore) SetMessageReactions(ctx context.Context, req domain.SetPr
|
|||
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
}
|
||||
if req.Peer.ID == req.UserID {
|
||||
return s.setSavedMessageTags(ctx, req)
|
||||
}
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("set message reactions: db does not support transactions")
|
||||
|
|
@ -240,10 +243,17 @@ func (s *MessageStore) enrichPrivateMessageReactions(ctx context.Context, db sql
|
|||
if err := s.enrichPrivateMessagePolls(ctx, db, viewerUserID, messages); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.enrichSavedMessageTags(ctx, db, messages); err != nil {
|
||||
return err
|
||||
}
|
||||
keySet := make(map[privateMessageReactionKey]struct{}, len(messages))
|
||||
senderIDs := make([]int64, 0, len(messages))
|
||||
privateIDs := make([]int64, 0, len(messages))
|
||||
for _, msg := range messages {
|
||||
if msg.OwnerUserID != 0 &&
|
||||
msg.Peer == (domain.Peer{Type: domain.PeerTypeUser, ID: msg.OwnerUserID}) {
|
||||
continue
|
||||
}
|
||||
if msg.UID == 0 || msg.From.ID == 0 {
|
||||
continue
|
||||
}
|
||||
|
|
@ -420,6 +430,11 @@ func writeMessageReactionsHash(h hash.Hash64, reactions *domain.ChannelMessageRe
|
|||
return
|
||||
}
|
||||
var buf [16]byte
|
||||
if reactions.AsTags {
|
||||
_, _ = h.Write([]byte{1})
|
||||
} else {
|
||||
_, _ = h.Write([]byte{0})
|
||||
}
|
||||
for _, item := range reactions.Results {
|
||||
_, _ = h.Write([]byte(item.Reaction.Type))
|
||||
_, _ = h.Write([]byte{0})
|
||||
|
|
|
|||
308
internal/store/postgres/message_saved_reactions.go
Normal file
308
internal/store/postgres/message_saved_reactions.go
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
func (s *MessageStore) setSavedMessageTags(ctx context.Context, req domain.SetPrivateMessageReactionsRequest) (domain.PrivateMessageReactionsResult, error) {
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("set saved message tags: db does not support transactions")
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("begin set saved message tags tx: %w", err)
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback(ctx)
|
||||
}
|
||||
}()
|
||||
if err := lockUsersForUpdate(ctx, tx, req.UserID); err != nil {
|
||||
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("lock saved message tag owner: %w", err)
|
||||
}
|
||||
|
||||
var boxID int32
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT box_id
|
||||
FROM message_boxes
|
||||
WHERE owner_user_id = $1
|
||||
AND box_id = $2
|
||||
AND peer_type = 'user'
|
||||
AND peer_id = $1
|
||||
AND NOT deleted
|
||||
LIMIT 1
|
||||
FOR UPDATE`, req.UserID, int32(req.MessageID)).Scan(&boxID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("get saved message for tags: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
DELETE FROM saved_message_reaction_tags
|
||||
WHERE user_id = $1 AND message_box_id = $2`, req.UserID, boxID); err != nil {
|
||||
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("delete old saved message tags: %w", err)
|
||||
}
|
||||
for i, reaction := range req.Reactions {
|
||||
if !reaction.Valid() {
|
||||
return domain.PrivateMessageReactionsResult{}, domain.ErrReactionInvalid
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO saved_message_reaction_tags (
|
||||
user_id, message_box_id, reaction_type, reaction_value, chosen_order
|
||||
) VALUES ($1, $2, $3, $4, $5)`,
|
||||
req.UserID, boxID, string(reaction.Type), reaction.Value(), int32(i+1)); err != nil {
|
||||
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("insert saved message tag: %w", err)
|
||||
}
|
||||
}
|
||||
rows, err := sqlcgen.New(tx).GetMessageBoxesByIDs(ctx, sqlcgen.GetMessageBoxesByIDsParams{
|
||||
OwnerUserID: req.UserID,
|
||||
BoxIds: []int32{boxID},
|
||||
})
|
||||
if err != nil {
|
||||
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("reload saved message tags box: %w", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
msg, err := messageFromIDRow(rows[0])
|
||||
if err != nil {
|
||||
return domain.PrivateMessageReactionsResult{}, err
|
||||
}
|
||||
messages := []domain.Message{msg}
|
||||
if err := s.enrichPrivateMessageReactions(ctx, tx, req.UserID, messages); err != nil {
|
||||
return domain.PrivateMessageReactionsResult{}, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return domain.PrivateMessageReactionsResult{}, fmt.Errorf("commit saved message tags tx: %w", err)
|
||||
}
|
||||
committed = true
|
||||
reactions := domain.ChannelMessageReactions{AsTags: true}
|
||||
if messages[0].Reactions != nil {
|
||||
reactions = *messages[0].Reactions
|
||||
}
|
||||
return domain.PrivateMessageReactionsResult{
|
||||
Messages: messages,
|
||||
Reactions: reactions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MessageStore) enrichSavedMessageTags(ctx context.Context, db sqlcgen.DBTX, messages []domain.Message) error {
|
||||
ownerIDs := make([]int64, 0, len(messages))
|
||||
boxIDs := make([]int32, 0, len(messages))
|
||||
indexes := make(map[[2]int64]int, len(messages))
|
||||
for i := range messages {
|
||||
msg := messages[i]
|
||||
if msg.OwnerUserID == 0 ||
|
||||
msg.Peer != (domain.Peer{Type: domain.PeerTypeUser, ID: msg.OwnerUserID}) {
|
||||
continue
|
||||
}
|
||||
ownerIDs = append(ownerIDs, msg.OwnerUserID)
|
||||
boxIDs = append(boxIDs, int32(msg.ID))
|
||||
indexes[[2]int64{msg.OwnerUserID, int64(msg.ID)}] = i
|
||||
}
|
||||
if len(ownerIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
rows, err := db.Query(ctx, `
|
||||
WITH wanted AS (
|
||||
SELECT user_id, message_box_id
|
||||
FROM unnest($1::bigint[], $2::int[]) AS w(user_id, message_box_id)
|
||||
)
|
||||
SELECT t.user_id, t.message_box_id, t.reaction_type, t.reaction_value, t.chosen_order
|
||||
FROM saved_message_reaction_tags t
|
||||
JOIN wanted w
|
||||
ON w.user_id = t.user_id
|
||||
AND w.message_box_id = t.message_box_id
|
||||
ORDER BY t.user_id, t.message_box_id, t.chosen_order, t.reaction_type, t.reaction_value`,
|
||||
ownerIDs, boxIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load saved message tags: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
userID int64
|
||||
messageBoxID int32
|
||||
reactionType string
|
||||
reactionValue string
|
||||
chosenOrder int32
|
||||
)
|
||||
if err := rows.Scan(&userID, &messageBoxID, &reactionType, &reactionValue, &chosenOrder); err != nil {
|
||||
return fmt.Errorf("scan saved message tag: %w", err)
|
||||
}
|
||||
reaction, ok := domain.MessageReactionFromValue(domain.MessageReactionType(reactionType), reactionValue)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
index, ok := indexes[[2]int64{userID, int64(messageBoxID)}]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if messages[index].Reactions == nil {
|
||||
messages[index].Reactions = &domain.ChannelMessageReactions{
|
||||
AsTags: true,
|
||||
Results: []domain.ChannelMessageReactionCount{},
|
||||
Recent: []domain.ChannelMessagePeerReaction{},
|
||||
}
|
||||
}
|
||||
messages[index].Reactions.Results = append(messages[index].Reactions.Results, domain.ChannelMessageReactionCount{
|
||||
Reaction: reaction,
|
||||
Count: 1,
|
||||
ChosenOrder: int(chosenOrder),
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("saved message tag rows: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MessageStore) ListSavedReactionTags(ctx context.Context, req domain.SavedReactionTagsRequest) ([]domain.SavedReactionTag, error) {
|
||||
if req.UserID == 0 {
|
||||
return nil, domain.ErrReactionInvalid
|
||||
}
|
||||
if req.Limit <= 0 || req.Limit > domain.MaxSavedReactionTags {
|
||||
req.Limit = domain.MaxSavedReactionTags
|
||||
}
|
||||
savedPeerType := ""
|
||||
var savedPeerID int64
|
||||
if req.SavedPeer.ID != 0 {
|
||||
savedPeerType = string(req.SavedPeer.Type)
|
||||
savedPeerID = req.SavedPeer.ID
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT
|
||||
a.reaction_type,
|
||||
a.reaction_value,
|
||||
CASE WHEN $2 = '' THEN COALESCE(t.title, '') ELSE '' END AS title,
|
||||
COUNT(*)::int AS reaction_count
|
||||
FROM saved_message_reaction_tags a
|
||||
JOIN message_boxes m
|
||||
ON m.owner_user_id = a.user_id
|
||||
AND m.box_id = a.message_box_id
|
||||
AND NOT m.deleted
|
||||
AND m.peer_type = 'user'
|
||||
AND m.peer_id = a.user_id
|
||||
LEFT JOIN user_saved_reaction_tags t
|
||||
ON t.user_id = a.user_id
|
||||
AND t.reaction_type = a.reaction_type
|
||||
AND t.reaction_value = a.reaction_value
|
||||
WHERE a.user_id = $1
|
||||
AND ($2 = '' OR (m.saved_peer_type = $2 AND m.saved_peer_id = $3))
|
||||
GROUP BY a.reaction_type, a.reaction_value, title
|
||||
ORDER BY
|
||||
reaction_count DESC,
|
||||
CASE
|
||||
WHEN a.reaction_type = 'custom_emoji'
|
||||
THEN lpad(to_hex(a.reaction_value::bigint), 16, '0')
|
||||
ELSE substr(md5(replace(a.reaction_value, U&'\FE0F', '')), 1, 16)
|
||||
END DESC
|
||||
LIMIT $4`, req.UserID, savedPeerType, savedPeerID, int32(req.Limit))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list saved reaction tags: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]domain.SavedReactionTag, 0, req.Limit)
|
||||
for rows.Next() {
|
||||
var reactionType, reactionValue, title string
|
||||
var count int32
|
||||
if err := rows.Scan(&reactionType, &reactionValue, &title, &count); err != nil {
|
||||
return nil, fmt.Errorf("scan saved reaction tag: %w", err)
|
||||
}
|
||||
reaction, ok := domain.MessageReactionFromValue(domain.MessageReactionType(reactionType), reactionValue)
|
||||
if !ok || count <= 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, domain.SavedReactionTag{
|
||||
UserID: req.UserID,
|
||||
Reaction: reaction,
|
||||
Title: title,
|
||||
Count: int(count),
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("saved reaction tag rows: %w", err)
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].Count != out[j].Count {
|
||||
return out[i].Count > out[j].Count
|
||||
}
|
||||
return out[i].Reaction.Key() > out[j].Reaction.Key()
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MessageStore) UpsertSavedReactionTag(ctx context.Context, tag domain.SavedReactionTag) error {
|
||||
if tag.UserID == 0 || !tag.Reaction.Valid() || utf8.RuneCountInString(tag.Title) > 12 {
|
||||
return domain.ErrReactionInvalid
|
||||
}
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return fmt.Errorf("update saved reaction tag title: db does not support transactions")
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin update saved reaction tag title tx: %w", err)
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback(ctx)
|
||||
}
|
||||
}()
|
||||
if err := lockUsersForUpdate(ctx, tx, tag.UserID); err != nil {
|
||||
return fmt.Errorf("lock saved reaction tag owner: %w", err)
|
||||
}
|
||||
var exists bool
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM saved_message_reaction_tags a
|
||||
JOIN message_boxes m
|
||||
ON m.owner_user_id = a.user_id
|
||||
AND m.box_id = a.message_box_id
|
||||
AND NOT m.deleted
|
||||
AND m.peer_type = 'user'
|
||||
AND m.peer_id = a.user_id
|
||||
WHERE a.user_id = $1
|
||||
AND a.reaction_type = $2
|
||||
AND a.reaction_value = $3
|
||||
)`, tag.UserID, string(tag.Reaction.Type), tag.Reaction.Value()).Scan(&exists); err != nil {
|
||||
return fmt.Errorf("check saved reaction tag assignment: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return domain.ErrReactionInvalid
|
||||
}
|
||||
if tag.Title == "" {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
DELETE FROM user_saved_reaction_tags
|
||||
WHERE user_id = $1 AND reaction_type = $2 AND reaction_value = $3`,
|
||||
tag.UserID, string(tag.Reaction.Type), tag.Reaction.Value()); err != nil {
|
||||
return fmt.Errorf("delete saved reaction tag title: %w", err)
|
||||
}
|
||||
} else if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO user_saved_reaction_tags (
|
||||
user_id, reaction_type, reaction_value, title, reaction_count
|
||||
) VALUES ($1, $2, $3, $4, 0)
|
||||
ON CONFLICT (user_id, reaction_type, reaction_value)
|
||||
DO UPDATE SET title = EXCLUDED.title, reaction_count = 0, updated_at = now()`,
|
||||
tag.UserID, string(tag.Reaction.Type), tag.Reaction.Value(), tag.Title); err != nil {
|
||||
return fmt.Errorf("upsert saved reaction tag title: %w", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit saved reaction tag title tx: %w", err)
|
||||
}
|
||||
committed = true
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestSavedMessageTagsPostgresAssignmentCountsSearchAndDelete(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
users := NewUserStore(pool)
|
||||
user, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 1,
|
||||
Phone: "+1777" + suffix + "01",
|
||||
FirstName: "SavedTags",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create saved-tag user: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM saved_message_reaction_tags WHERE user_id = $1", user.ID)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM user_saved_reaction_tags WHERE user_id = $1", user.ID)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM message_boxes WHERE owner_user_id = $1", user.ID)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM private_messages WHERE sender_user_id = $1 OR recipient_user_id = $1", user.ID)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM user_update_events WHERE user_id = $1", user.ID)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM dialogs WHERE user_id = $1", user.ID)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", user.ID)
|
||||
})
|
||||
|
||||
messages := NewMessageStore(pool)
|
||||
self := domain.Peer{Type: domain.PeerTypeUser, ID: user.ID}
|
||||
peerA := domain.Peer{Type: domain.PeerTypeUser, ID: user.ID}
|
||||
peerB := domain.Peer{Type: domain.PeerTypeChannel, ID: 90001}
|
||||
create := func(body string, savedPeer domain.Peer) domain.Message {
|
||||
msg, err := messages.Create(ctx, domain.Message{
|
||||
OwnerUserID: user.ID,
|
||||
Peer: self,
|
||||
From: self,
|
||||
Date: int(time.Now().Unix()),
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create saved message: %v", err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `
|
||||
UPDATE message_boxes
|
||||
SET saved_peer_type = $3, saved_peer_id = $4
|
||||
WHERE owner_user_id = $1 AND box_id = $2`,
|
||||
user.ID, msg.ID, string(savedPeer.Type), savedPeer.ID); err != nil {
|
||||
t.Fatalf("set saved peer: %v", err)
|
||||
}
|
||||
msg.SavedPeer = savedPeer
|
||||
return msg
|
||||
}
|
||||
first := create("first", peerA)
|
||||
second := create("second", peerA)
|
||||
third := create("third", peerB)
|
||||
thumb := domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "👍"}
|
||||
custom := domain.MessageReaction{Type: domain.MessageReactionCustomEmoji, DocumentID: 70001}
|
||||
|
||||
set := func(msg domain.Message, reactions ...domain.MessageReaction) {
|
||||
t.Helper()
|
||||
result, err := messages.SetMessageReactions(ctx, domain.SetPrivateMessageReactionsRequest{
|
||||
UserID: user.ID,
|
||||
Peer: self,
|
||||
MessageID: msg.ID,
|
||||
Reactions: reactions,
|
||||
ReactionsPerUserMax: 3,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("set saved tags on %d: %v", msg.ID, err)
|
||||
}
|
||||
if len(result.Messages) != 1 || result.Messages[0].Reactions == nil ||
|
||||
!result.Messages[0].Reactions.AsTags {
|
||||
t.Fatalf("set saved tags result = %+v", result)
|
||||
}
|
||||
}
|
||||
set(first, thumb)
|
||||
set(second, thumb, custom)
|
||||
set(third, custom)
|
||||
if err := messages.UpsertSavedReactionTag(ctx, domain.SavedReactionTag{
|
||||
UserID: user.ID, Reaction: custom, Title: "Custom",
|
||||
}); err != nil {
|
||||
t.Fatalf("rename custom saved tag: %v", err)
|
||||
}
|
||||
|
||||
global, err := messages.ListSavedReactionTags(ctx, domain.SavedReactionTagsRequest{
|
||||
UserID: user.ID, Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list global saved tags: %v", err)
|
||||
}
|
||||
assertPostgresSavedTag(t, global, thumb, 2, "")
|
||||
assertPostgresSavedTag(t, global, custom, 2, "Custom")
|
||||
|
||||
perPeer, err := messages.ListSavedReactionTags(ctx, domain.SavedReactionTagsRequest{
|
||||
UserID: user.ID, SavedPeer: peerA, Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list per-peer saved tags: %v", err)
|
||||
}
|
||||
assertPostgresSavedTag(t, perPeer, thumb, 2, "")
|
||||
assertPostgresSavedTag(t, perPeer, custom, 1, "")
|
||||
|
||||
search, err := messages.ListByUser(ctx, user.ID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: self,
|
||||
SavedPeer: peerA,
|
||||
SavedReactions: []domain.MessageReaction{custom},
|
||||
NeedTotalCount: true,
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("search saved tag: %v", err)
|
||||
}
|
||||
if len(search.Messages) != 1 || search.Messages[0].ID != second.ID ||
|
||||
search.Messages[0].Reactions == nil || !search.Messages[0].Reactions.AsTags {
|
||||
t.Fatalf("saved tag search = %+v, want second message", search.Messages)
|
||||
}
|
||||
searchAny, err := messages.ListByUser(ctx, user.ID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: self,
|
||||
SavedReactions: []domain.MessageReaction{thumb, custom},
|
||||
NeedTotalCount: true,
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("search any saved tag: %v", err)
|
||||
}
|
||||
if len(searchAny.Messages) != 3 || searchAny.Count != 3 {
|
||||
t.Fatalf("saved tag OR search = count %d messages %+v, want all three", searchAny.Count, searchAny.Messages)
|
||||
}
|
||||
|
||||
var reactionEvents int
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT COUNT(*)::int
|
||||
FROM user_update_events
|
||||
WHERE user_id = $1 AND event_type = 'message_reactions'`, user.ID).Scan(&reactionEvents); err != nil {
|
||||
t.Fatalf("count reaction events: %v", err)
|
||||
}
|
||||
if reactionEvents != 0 {
|
||||
t.Fatalf("reaction durable events = %d, want 0", reactionEvents)
|
||||
}
|
||||
|
||||
if _, err := messages.DeleteMessages(ctx, domain.DeleteMessagesRequest{
|
||||
OwnerUserID: user.ID,
|
||||
IDs: []int{second.ID},
|
||||
Date: int(time.Now().Unix()),
|
||||
}); err != nil {
|
||||
t.Fatalf("delete tagged saved message: %v", err)
|
||||
}
|
||||
global, err = messages.ListSavedReactionTags(ctx, domain.SavedReactionTagsRequest{
|
||||
UserID: user.ID, Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list tags after delete: %v", err)
|
||||
}
|
||||
assertPostgresSavedTag(t, global, thumb, 1, "")
|
||||
assertPostgresSavedTag(t, global, custom, 1, "Custom")
|
||||
}
|
||||
|
||||
func assertPostgresSavedTag(t *testing.T, tags []domain.SavedReactionTag, reaction domain.MessageReaction, count int, title string) {
|
||||
t.Helper()
|
||||
for _, tag := range tags {
|
||||
if tag.Reaction.Key() == reaction.Key() {
|
||||
if tag.Count != count || tag.Title != title {
|
||||
t.Fatalf("tag %s = %+v, want count=%d title=%q", reaction.Key(), tag, count, title)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("tag %s not found in %+v", reaction.Key(), tags)
|
||||
}
|
||||
|
|
@ -545,6 +545,8 @@ base AS NOT MATERIALIZED (
|
|||
sqlc.arg(query)::text = ''
|
||||
OR m.body ILIKE ('%' || sqlc.arg(query)::text || '%')
|
||||
)
|
||||
AND (sqlc.arg(min_date)::int <= 0 OR m.message_date > sqlc.arg(min_date)::int)
|
||||
AND (sqlc.arg(max_date)::int <= 0 OR m.message_date < sqlc.arg(max_date)::int)
|
||||
AND (sqlc.arg(max_id)::int <= 0 OR m.box_id < sqlc.arg(max_id)::int)
|
||||
AND (sqlc.arg(min_id)::int <= 0 OR m.box_id > sqlc.arg(min_id)::int)
|
||||
AND (NOT sqlc.arg(pinned_only)::boolean OR m.pinned)
|
||||
|
|
@ -564,6 +566,17 @@ base AS NOT MATERIALIZED (
|
|||
sqlc.arg(saved_peer_type)::text = ''
|
||||
OR (m.saved_peer_type = sqlc.arg(saved_peer_type)::text AND m.saved_peer_id = sqlc.arg(saved_peer_id)::bigint)
|
||||
)
|
||||
AND (
|
||||
cardinality(sqlc.arg(saved_reaction_keys)::text[]) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM saved_message_reaction_tags tag
|
||||
WHERE tag.user_id = m.owner_user_id
|
||||
AND tag.message_box_id = m.box_id
|
||||
AND (tag.reaction_type || ':' || tag.reaction_value)
|
||||
= ANY(sqlc.arg(saved_reaction_keys)::text[])
|
||||
)
|
||||
)
|
||||
),
|
||||
total AS (
|
||||
SELECT count(*)::int AS total_count
|
||||
|
|
@ -810,6 +823,8 @@ WHERE m.owner_user_id = sqlc.arg(owner_user_id)::bigint
|
|||
sqlc.arg(query)::text = ''
|
||||
OR m.body ILIKE ('%' || sqlc.arg(query)::text || '%')
|
||||
)
|
||||
AND (sqlc.arg(min_date)::int <= 0 OR m.message_date > sqlc.arg(min_date)::int)
|
||||
AND (sqlc.arg(max_date)::int <= 0 OR m.message_date < sqlc.arg(max_date)::int)
|
||||
AND (sqlc.arg(max_id)::int <= 0 OR m.box_id < sqlc.arg(max_id)::int)
|
||||
AND (sqlc.arg(min_id)::int <= 0 OR m.box_id > sqlc.arg(min_id)::int)
|
||||
AND (NOT sqlc.arg(pinned_only)::boolean OR m.pinned)
|
||||
|
|
@ -829,6 +844,17 @@ WHERE m.owner_user_id = sqlc.arg(owner_user_id)::bigint
|
|||
sqlc.arg(saved_peer_type)::text = ''
|
||||
OR (m.saved_peer_type = sqlc.arg(saved_peer_type)::text AND m.saved_peer_id = sqlc.arg(saved_peer_id)::bigint)
|
||||
)
|
||||
AND (
|
||||
cardinality(sqlc.arg(saved_reaction_keys)::text[]) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM saved_message_reaction_tags tag
|
||||
WHERE tag.user_id = m.owner_user_id
|
||||
AND tag.message_box_id = m.box_id
|
||||
AND (tag.reaction_type || ':' || tag.reaction_value)
|
||||
= ANY(sqlc.arg(saved_reaction_keys)::text[])
|
||||
)
|
||||
)
|
||||
AND (
|
||||
(sqlc.arg(offset_date)::int > 0 AND m.message_date < sqlc.arg(offset_date)::int)
|
||||
OR (sqlc.arg(offset_date)::int <= 0 AND (sqlc.arg(offset_id)::int <= 0 OR m.box_id < sqlc.arg(offset_id)::int))
|
||||
|
|
@ -856,6 +882,8 @@ WHERE m.owner_user_id = sqlc.arg(owner_user_id)::bigint
|
|||
sqlc.arg(query)::text = ''
|
||||
OR m.body ILIKE ('%' || sqlc.arg(query)::text || '%')
|
||||
)
|
||||
AND (sqlc.arg(min_date)::int <= 0 OR m.message_date > sqlc.arg(min_date)::int)
|
||||
AND (sqlc.arg(max_date)::int <= 0 OR m.message_date < sqlc.arg(max_date)::int)
|
||||
AND (sqlc.arg(max_id)::int <= 0 OR m.box_id < sqlc.arg(max_id)::int)
|
||||
AND (sqlc.arg(min_id)::int <= 0 OR m.box_id > sqlc.arg(min_id)::int)
|
||||
AND (NOT sqlc.arg(pinned_only)::boolean OR m.pinned)
|
||||
|
|
@ -874,6 +902,17 @@ WHERE m.owner_user_id = sqlc.arg(owner_user_id)::bigint
|
|||
AND (
|
||||
sqlc.arg(saved_peer_type)::text = ''
|
||||
OR (m.saved_peer_type = sqlc.arg(saved_peer_type)::text AND m.saved_peer_id = sqlc.arg(saved_peer_id)::bigint)
|
||||
)
|
||||
AND (
|
||||
cardinality(sqlc.arg(saved_reaction_keys)::text[]) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM saved_message_reaction_tags tag
|
||||
WHERE tag.user_id = m.owner_user_id
|
||||
AND tag.message_box_id = m.box_id
|
||||
AND (tag.reaction_type || ':' || tag.reaction_value)
|
||||
= ANY(sqlc.arg(saved_reaction_keys)::text[])
|
||||
)
|
||||
);
|
||||
|
||||
-- name: GetMessageBoxesByIDs :many
|
||||
|
|
|
|||
|
|
@ -26,11 +26,13 @@ WHERE m.owner_user_id = $1::bigint
|
|||
$7::text = ''
|
||||
OR m.body ILIKE ('%' || $7::text || '%')
|
||||
)
|
||||
AND ($8::int <= 0 OR m.box_id < $8::int)
|
||||
AND ($9::int <= 0 OR m.box_id > $9::int)
|
||||
AND (NOT $10::boolean OR m.pinned)
|
||||
AND ($8::int <= 0 OR m.message_date > $8::int)
|
||||
AND ($9::int <= 0 OR m.message_date < $9::int)
|
||||
AND ($10::int <= 0 OR m.box_id < $10::int)
|
||||
AND ($11::int <= 0 OR m.box_id > $11::int)
|
||||
AND (NOT $12::boolean OR m.pinned)
|
||||
AND (
|
||||
NOT $11::boolean
|
||||
NOT $13::boolean
|
||||
OR (
|
||||
m.media->>'kind' = 'document'
|
||||
AND EXISTS (
|
||||
|
|
@ -42,25 +44,39 @@ WHERE m.owner_user_id = $1::bigint
|
|||
)
|
||||
)
|
||||
AND (
|
||||
$12::text = ''
|
||||
OR (m.saved_peer_type = $12::text AND m.saved_peer_id = $13::bigint)
|
||||
$14::text = ''
|
||||
OR (m.saved_peer_type = $14::text AND m.saved_peer_id = $15::bigint)
|
||||
)
|
||||
AND (
|
||||
cardinality($16::text[]) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM saved_message_reaction_tags tag
|
||||
WHERE tag.user_id = m.owner_user_id
|
||||
AND tag.message_box_id = m.box_id
|
||||
AND (tag.reaction_type || ':' || tag.reaction_value)
|
||||
= ANY($16::text[])
|
||||
)
|
||||
)
|
||||
`
|
||||
|
||||
type CountMessagesByUserParams struct {
|
||||
OwnerUserID int64
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RestrictPeerIds bool
|
||||
PeerIds []int64
|
||||
Query string
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
OwnerUserID int64
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RestrictPeerIds bool
|
||||
PeerIds []int64
|
||||
Query string
|
||||
MinDate int32
|
||||
MaxDate int32
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
SavedReactionKeys []string
|
||||
}
|
||||
|
||||
// ListMessagesByUser total CTE 的独立化:相同 base 过滤(不含分页 anchor),
|
||||
|
|
@ -74,12 +90,15 @@ func (q *Queries) CountMessagesByUser(ctx context.Context, arg CountMessagesByUs
|
|||
arg.RestrictPeerIds,
|
||||
arg.PeerIds,
|
||||
arg.Query,
|
||||
arg.MinDate,
|
||||
arg.MaxDate,
|
||||
arg.MaxID,
|
||||
arg.MinID,
|
||||
arg.PinnedOnly,
|
||||
arg.MusicOnly,
|
||||
arg.SavedPeerType,
|
||||
arg.SavedPeerID,
|
||||
arg.SavedReactionKeys,
|
||||
)
|
||||
var total_count int32
|
||||
err := row.Scan(&total_count)
|
||||
|
|
@ -2295,11 +2314,13 @@ WHERE m.owner_user_id = $1::bigint
|
|||
$7::text = ''
|
||||
OR m.body ILIKE ('%' || $7::text || '%')
|
||||
)
|
||||
AND ($8::int <= 0 OR m.box_id < $8::int)
|
||||
AND ($9::int <= 0 OR m.box_id > $9::int)
|
||||
AND (NOT $10::boolean OR m.pinned)
|
||||
AND ($8::int <= 0 OR m.message_date > $8::int)
|
||||
AND ($9::int <= 0 OR m.message_date < $9::int)
|
||||
AND ($10::int <= 0 OR m.box_id < $10::int)
|
||||
AND ($11::int <= 0 OR m.box_id > $11::int)
|
||||
AND (NOT $12::boolean OR m.pinned)
|
||||
AND (
|
||||
NOT $11::boolean
|
||||
NOT $13::boolean
|
||||
OR (
|
||||
m.media->>'kind' = 'document'
|
||||
AND EXISTS (
|
||||
|
|
@ -2311,36 +2332,50 @@ WHERE m.owner_user_id = $1::bigint
|
|||
)
|
||||
)
|
||||
AND (
|
||||
$12::text = ''
|
||||
OR (m.saved_peer_type = $12::text AND m.saved_peer_id = $13::bigint)
|
||||
$14::text = ''
|
||||
OR (m.saved_peer_type = $14::text AND m.saved_peer_id = $15::bigint)
|
||||
)
|
||||
AND (
|
||||
($14::int > 0 AND m.message_date < $14::int)
|
||||
OR ($14::int <= 0 AND ($15::int <= 0 OR m.box_id < $15::int))
|
||||
cardinality($16::text[]) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM saved_message_reaction_tags tag
|
||||
WHERE tag.user_id = m.owner_user_id
|
||||
AND tag.message_box_id = m.box_id
|
||||
AND (tag.reaction_type || ':' || tag.reaction_value)
|
||||
= ANY($16::text[])
|
||||
)
|
||||
)
|
||||
AND (
|
||||
($17::int > 0 AND m.message_date < $17::int)
|
||||
OR ($17::int <= 0 AND ($18::int <= 0 OR m.box_id < $18::int))
|
||||
)
|
||||
ORDER BY m.box_id DESC
|
||||
OFFSET GREATEST($16::int, 0)
|
||||
LIMIT $17::int
|
||||
OFFSET GREATEST($19::int, 0)
|
||||
LIMIT $20::int
|
||||
`
|
||||
|
||||
type ListMessagesBackwardParams struct {
|
||||
OwnerUserID int64
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RestrictPeerIds bool
|
||||
PeerIds []int64
|
||||
Query string
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
OffsetDate int32
|
||||
OffsetID int32
|
||||
RowOffset int32
|
||||
LimitCount int32
|
||||
OwnerUserID int64
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RestrictPeerIds bool
|
||||
PeerIds []int64
|
||||
Query string
|
||||
MinDate int32
|
||||
MaxDate int32
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
SavedReactionKeys []string
|
||||
OffsetDate int32
|
||||
OffsetID int32
|
||||
RowOffset int32
|
||||
LimitCount int32
|
||||
}
|
||||
|
||||
type ListMessagesBackwardRow struct {
|
||||
|
|
@ -2433,12 +2468,15 @@ func (q *Queries) ListMessagesBackward(ctx context.Context, arg ListMessagesBack
|
|||
arg.RestrictPeerIds,
|
||||
arg.PeerIds,
|
||||
arg.Query,
|
||||
arg.MinDate,
|
||||
arg.MaxDate,
|
||||
arg.MaxID,
|
||||
arg.MinID,
|
||||
arg.PinnedOnly,
|
||||
arg.MusicOnly,
|
||||
arg.SavedPeerType,
|
||||
arg.SavedPeerID,
|
||||
arg.SavedReactionKeys,
|
||||
arg.OffsetDate,
|
||||
arg.OffsetID,
|
||||
arg.RowOffset,
|
||||
|
|
@ -2641,11 +2679,13 @@ base AS NOT MATERIALIZED (
|
|||
$11::text = ''
|
||||
OR m.body ILIKE ('%' || $11::text || '%')
|
||||
)
|
||||
AND ($12::int <= 0 OR m.box_id < $12::int)
|
||||
AND ($13::int <= 0 OR m.box_id > $13::int)
|
||||
AND (NOT $14::boolean OR m.pinned)
|
||||
AND ($12::int <= 0 OR m.message_date > $12::int)
|
||||
AND ($13::int <= 0 OR m.message_date < $13::int)
|
||||
AND ($14::int <= 0 OR m.box_id < $14::int)
|
||||
AND ($15::int <= 0 OR m.box_id > $15::int)
|
||||
AND (NOT $16::boolean OR m.pinned)
|
||||
AND (
|
||||
NOT $15::boolean
|
||||
NOT $17::boolean
|
||||
OR (
|
||||
m.media->>'kind' = 'document'
|
||||
AND EXISTS (
|
||||
|
|
@ -2657,14 +2697,25 @@ base AS NOT MATERIALIZED (
|
|||
)
|
||||
)
|
||||
AND (
|
||||
$16::text = ''
|
||||
OR (m.saved_peer_type = $16::text AND m.saved_peer_id = $17::bigint)
|
||||
$18::text = ''
|
||||
OR (m.saved_peer_type = $18::text AND m.saved_peer_id = $19::bigint)
|
||||
)
|
||||
AND (
|
||||
cardinality($20::text[]) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM saved_message_reaction_tags tag
|
||||
WHERE tag.user_id = m.owner_user_id
|
||||
AND tag.message_box_id = m.box_id
|
||||
AND (tag.reaction_type || ':' || tag.reaction_value)
|
||||
= ANY($20::text[])
|
||||
)
|
||||
)
|
||||
),
|
||||
total AS (
|
||||
SELECT count(*)::int AS total_count
|
||||
FROM base
|
||||
WHERE $18::boolean
|
||||
WHERE $21::boolean
|
||||
),
|
||||
backward AS (
|
||||
SELECT b.box_id, b.private_message_id, b.owner_user_id, b.peer_type, b.peer_id, b.from_user_id, b.message_date, b.ttl_period, b.expires_at, b.edit_date, b.hide_edited, b.outgoing, b.body, b.entities_json, b.silent, b.noforwards, b.reply_to_msg_id, b.reply_to_peer_type, b.reply_to_peer_id, b.reply_to_top_id, b.reply_to_story_id, b.quote_text, b.quote_entities_json, b.quote_offset, b.fwd_from_peer_type, b.fwd_from_peer_id, b.fwd_from_name, b.fwd_date, b.fwd_saved_from_peer_type, b.fwd_saved_from_peer_id, b.fwd_saved_from_msg_id, b.saved_peer_type, b.saved_peer_id, b.pts, b.media_json, b.media_unread, b.reaction_unread, b.pinned, b.via_bot_id, b.grouped_id, b.effect, b.reply_markup_json, b.rich_message_json, b.peer_user_id, b.peer_access_hash, b.peer_phone, b.peer_first_name, b.peer_last_name, b.peer_username, b.peer_country_code, b.peer_verified, b.peer_support, b.peer_is_bot, b.peer_bot_info_version, b.peer_premium_until, b.peer_emoji_status_document_id, b.peer_emoji_status_until, b.peer_last_seen_at, b.from_user_user_id, b.from_user_access_hash, b.from_user_phone, b.from_user_first_name, b.from_user_last_name, b.from_user_username, b.from_user_country_code, b.from_user_verified, b.from_user_support, b.from_user_is_bot, b.from_user_bot_info_version, b.from_user_premium_until, b.from_user_emoji_status_document_id, b.from_user_emoji_status_until, b.from_user_last_seen_at
|
||||
|
|
@ -2811,24 +2862,27 @@ ORDER BY box_id DESC
|
|||
`
|
||||
|
||||
type ListMessagesByUserParams struct {
|
||||
OwnerUserID int64
|
||||
OffsetID int32
|
||||
OffsetDate int32
|
||||
AddOffset int32
|
||||
LimitCount int32
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RestrictPeerIds bool
|
||||
PeerIds []int64
|
||||
Query string
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
NeedTotalCount bool
|
||||
OwnerUserID int64
|
||||
OffsetID int32
|
||||
OffsetDate int32
|
||||
AddOffset int32
|
||||
LimitCount int32
|
||||
HasPeer bool
|
||||
PeerType string
|
||||
PeerID int64
|
||||
RestrictPeerIds bool
|
||||
PeerIds []int64
|
||||
Query string
|
||||
MinDate int32
|
||||
MaxDate int32
|
||||
MaxID int32
|
||||
MinID int32
|
||||
PinnedOnly bool
|
||||
MusicOnly bool
|
||||
SavedPeerType string
|
||||
SavedPeerID int64
|
||||
SavedReactionKeys []string
|
||||
NeedTotalCount bool
|
||||
}
|
||||
|
||||
type ListMessagesByUserRow struct {
|
||||
|
|
@ -2921,12 +2975,15 @@ func (q *Queries) ListMessagesByUser(ctx context.Context, arg ListMessagesByUser
|
|||
arg.RestrictPeerIds,
|
||||
arg.PeerIds,
|
||||
arg.Query,
|
||||
arg.MinDate,
|
||||
arg.MaxDate,
|
||||
arg.MaxID,
|
||||
arg.MinID,
|
||||
arg.PinnedOnly,
|
||||
arg.MusicOnly,
|
||||
arg.SavedPeerType,
|
||||
arg.SavedPeerID,
|
||||
arg.SavedReactionKeys,
|
||||
arg.NeedTotalCount,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("migrate star gift lifecycle schema: %v", err)
|
||||
}
|
||||
if status.Dirty || status.Empty || status.Version != 147 {
|
||||
t.Fatalf("migration status = %+v, want clean version 147", status)
|
||||
if status.Dirty || status.Empty || status.Version != 148 {
|
||||
t.Fatalf("migration status = %+v, want clean version 148", status)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue