perf: sync megagroup unread derivation

This commit is contained in:
A 2026-07-09 13:49:34 +08:00
parent 4c0cc2b7a7
commit 41c7f1d018
5 changed files with 87 additions and 217 deletions

View file

@ -902,14 +902,23 @@ func (s *ChannelStore) CountChannelArchiveUnread(ctx context.Context, userID int
}
var peers, messages int32
// JOIN active member退群残留的 channel_dialogs 行不计入归档徽章。
// unread 读时动态派生H4a不再消费 channel_dialogs.unread_count 缓存列;归档集合
// 有界(需显式归档建行),每行动态 COUNT 已被 cap 钳制。
visibleTopID := "CASE WHEN c.top_message_id > m.available_min_id THEN c.top_message_id ELSE 0 END"
visibleReadInbox := "GREATEST(COALESCE(d.read_inbox_max_id, 0), m.read_inbox_max_id)"
if err := s.db.QueryRow(ctx, `
SELECT
COUNT(*) FILTER (WHERE d.unread_count > 0 OR d.unread_mark)::int,
COALESCE(SUM(d.unread_count), 0)::int
FROM channel_dialogs d
JOIN channel_members m ON m.channel_id = d.channel_id AND m.user_id = d.user_id AND m.status = 'active'
WHERE d.user_id = $1
AND COALESCE(d.folder_id, 0) = 1`, userID).Scan(&peers, &messages); err != nil {
COUNT(*) FILTER (WHERE t.unread_count > 0 OR t.unread_mark)::int,
COALESCE(SUM(t.unread_count), 0)::int
FROM (
SELECT `+channelDialogVisibleUnreadCountSQL(visibleReadInbox, visibleTopID)+` AS unread_count,
COALESCE(d.unread_mark, m.unread_mark) AS unread_mark
FROM channel_dialogs d
JOIN channel_members m ON m.channel_id = d.channel_id AND m.user_id = d.user_id AND m.status = 'active'
JOIN channels c ON c.id = d.channel_id AND NOT c.deleted
WHERE d.user_id = $1
AND COALESCE(d.folder_id, 0) = 1
) t`, userID).Scan(&peers, &messages); err != nil {
return 0, 0, fmt.Errorf("count channel archive unread: %w", err)
}
return int(peers), int(messages), nil
@ -1112,61 +1121,6 @@ func cappedChannelUnreadCount(whereBody string) string {
)`, whereBody, domain.MaxDialogUnreadCount)
}
func refreshChannelDialogsAfterDeleteTx(ctx context.Context, tx pgx.Tx, channel domain.Channel) error {
if channel.ID == 0 || !shouldSynchronouslyUpsertChannelDialogs(channel) {
return nil
}
insertUnread := cappedChannelUnreadCount(`WHERE msg.channel_id = $1
AND msg.id > GREATEST(active.read_inbox_max_id, active.available_min_id)
AND msg.id <= active.visible_top_id
AND NOT msg.deleted
AND msg.sender_user_id <> active.user_id`)
conflictUnread := cappedChannelUnreadCount(`WHERE msg.channel_id = channel_dialogs.channel_id
AND msg.id > GREATEST(channel_dialogs.read_inbox_max_id, EXCLUDED.read_inbox_max_id)
AND msg.id <= EXCLUDED.top_message_id
AND NOT msg.deleted
AND msg.sender_user_id <> channel_dialogs.user_id`)
if _, err := tx.Exec(ctx, fmt.Sprintf(`
WITH active AS (
SELECT
m.user_id,
m.available_min_id,
GREATEST(COALESCE(d.read_inbox_max_id, 0), m.read_inbox_max_id) AS read_inbox_max_id,
LEAST(GREATEST(c.top_message_id, 0), GREATEST(COALESCE(d.read_outbox_max_id, 0), m.read_outbox_max_id, CASE WHEN c.read_inbox_top1_user_id = m.user_id THEN c.read_inbox_top2 ELSE c.read_inbox_top1 END)) AS read_outbox_max_id,
COALESCE(d.unread_mark, m.unread_mark) AS unread_mark,
CASE WHEN $2 > m.available_min_id THEN $2 ELSE 0 END AS visible_top_id
FROM channel_members m
JOIN channels c ON c.id = m.channel_id
LEFT JOIN channel_dialogs d ON d.user_id = m.user_id AND d.channel_id = m.channel_id
WHERE m.channel_id = $1
AND m.status = 'active'
AND NOT COALESCE((m.banned_rights->>'ViewMessages')::boolean, false)
)
INSERT INTO channel_dialogs (
user_id, channel_id, top_message_id, top_message_date,
read_inbox_max_id, read_outbox_max_id, unread_count, unread_mark
)
SELECT
user_id, $1, visible_top_id,
CASE WHEN visible_top_id > 0 THEN COALESCE(top_msg.message_date, $3) ELSE 0 END,
read_inbox_max_id, read_outbox_max_id,
%s,
unread_mark
FROM active
LEFT JOIN channel_messages top_msg ON top_msg.channel_id = $1 AND top_msg.id = active.visible_top_id AND NOT top_msg.deleted
ON CONFLICT (user_id, channel_id) DO UPDATE SET
top_message_id = EXCLUDED.top_message_id,
top_message_date = EXCLUDED.top_message_date,
read_inbox_max_id = GREATEST(channel_dialogs.read_inbox_max_id, EXCLUDED.read_inbox_max_id),
read_outbox_max_id = GREATEST(channel_dialogs.read_outbox_max_id, EXCLUDED.read_outbox_max_id),
unread_count = %s,
unread_mark = EXCLUDED.unread_mark,
updated_at = now()`, insertUnread, conflictUnread), channel.ID, channel.TopMessageID, channel.Date); err != nil {
return fmt.Errorf("refresh channel dialogs after delete: %w", err)
}
return nil
}
func upsertChannelDialogTx(ctx context.Context, tx pgx.Tx, userID int64, channel domain.Channel, top domain.ChannelMessage, readInboxMaxID, readOutboxMaxID int) error {
topDate := top.Date
if topDate == 0 {
@ -1199,6 +1153,18 @@ ON CONFLICT (user_id, channel_id) DO UPDATE SET
return nil
}
// upsertChannelDialogsForMessageTx 在发送事务内处理消息的收件人侧读边界副作用。
//
// 性能审计 H4a不再对全员旧闸门为 megagroup ≤ MaxSynchronousChannelDialogFanout
// upsert channel_dialogs——发送事务 O(成员) 行写 + 行锁是放开大群的阻断项P1-u 残余)。
// 所有频道broadcast / megagroup 不分大小统一读时派生unread 由可见 incoming 消息
// 重算channelDialogDynamicUnreadCountSQLcappedtop 由 channels.top_message_id 提供,
// read boundary 真值在 channel_membersdialog 行仅承载 pin/folder/unread_mark/draft 等
// 用户显式状态并按需惰性建行。
//
// 保留的唯一逐收件人写skipDeliveryprivacy bot成员的 read_inbox 边界推进——这些
// 成员被排除投递,必须同步抬边界避免其未读派生把被隐藏消息计成 unread集合有界
// bot-only 候选)。
func upsertChannelDialogsForMessageTx(ctx context.Context, tx pgx.Tx, channel domain.Channel, top domain.ChannelMessage, selfReadUserID int64, skipDeliveryUserIDs ...[]int64) error {
if channel.ID == 0 || top.ID == 0 {
return nil
@ -1221,75 +1187,11 @@ WHERE channel_id = $1
return fmt.Errorf("advance skipped channel delivery boundary: %w", err)
}
}
if !shouldSynchronouslyUpsertChannelDialogs(channel) {
return nil
}
topDate := top.Date
if topDate == 0 {
topDate = channel.Date
}
// 下界用 GREATEST(read_inbox, available_min_id) 与 delete/read 路径一致:当前
// 全局不变量 read_inbox_max_id >= available_min_id 使其为 no-op,但该不变量未由
// schema 强制,对齐后即便未来有路径只抬 available_min 不抬 read 也不会 over-count。
insertUnread := cappedChannelUnreadCount(`WHERE msg.channel_id = $1
AND msg.id > GREATEST(active.read_inbox_max_id, active.available_min_id)
AND msg.id <= $2
AND NOT msg.deleted
AND msg.sender_user_id <> active.user_id`)
conflictUnread := cappedChannelUnreadCount(`WHERE msg.channel_id = channel_dialogs.channel_id
AND msg.id > GREATEST(channel_dialogs.read_inbox_max_id, EXCLUDED.read_inbox_max_id)
AND msg.id <= GREATEST(channel_dialogs.top_message_id, EXCLUDED.top_message_id)
AND NOT msg.deleted
AND msg.sender_user_id <> channel_dialogs.user_id`)
if _, err := tx.Exec(ctx, fmt.Sprintf(`
WITH active AS (
SELECT
m.user_id,
CASE
WHEN m.user_id = $4 THEN GREATEST(m.read_inbox_max_id, $2)
ELSE m.read_inbox_max_id
END AS read_inbox_max_id,
m.available_min_id,
-- sender outbox 回执水位绝不随自己发消息推进read_outbox_max_id
-- 语义是"已被其他成员读到的最大 ID"推进只能来自对端 readHistory
m.read_outbox_max_id
FROM channel_members m
WHERE m.channel_id = $1
AND m.status = 'active'
AND NOT COALESCE((m.banned_rights->>'ViewMessages')::boolean, false)
AND $2 > m.available_min_id
AND NOT (m.user_id = ANY($5::bigint[]))
)
INSERT INTO channel_dialogs (
user_id, channel_id, top_message_id, top_message_date,
read_inbox_max_id, read_outbox_max_id, unread_count, unread_mark
)
SELECT
user_id, $1, $2, $3,
read_inbox_max_id, read_outbox_max_id,
%s,
false
FROM active
ON CONFLICT (user_id, channel_id) DO UPDATE SET
top_message_id = GREATEST(channel_dialogs.top_message_id, EXCLUDED.top_message_id),
top_message_date = GREATEST(channel_dialogs.top_message_date, EXCLUDED.top_message_date),
read_inbox_max_id = GREATEST(channel_dialogs.read_inbox_max_id, EXCLUDED.read_inbox_max_id),
read_outbox_max_id = GREATEST(channel_dialogs.read_outbox_max_id, EXCLUDED.read_outbox_max_id),
unread_count = %s,
unread_mark = CASE WHEN channel_dialogs.user_id = $4 THEN false ELSE channel_dialogs.unread_mark END,
updated_at = now()`, insertUnread, conflictUnread), channel.ID, top.ID, topDate, selfReadUserID, skipIDs); err != nil {
return fmt.Errorf("upsert channel message dialogs: %w", err)
}
// skipDelivery 成员可能已有 channel_dialogs 行pin/归档等惰性建行),其 read_inbox
// 经 GREATEST(d, m) 读取——member 行已推进即可dialog 行无需同步。
return nil
}
func shouldSynchronouslyUpsertChannelDialogs(channel domain.Channel) bool {
if channel.Broadcast {
return false
}
return channel.ParticipantsCount > 0 && channel.ParticipantsCount <= domain.MaxSynchronousChannelDialogFanout
}
func scanChannelDialogRow(row rowScanner, userID int64) (domain.Channel, domain.Dialog, *domain.Peer, error) {
var ch domain.Channel
var rights, reactionPolicy string

View file

@ -411,9 +411,8 @@ RETURNING pinned_message_id`, channel.ID, topID, pts).Scan(&latestPinned); err !
channel.TopMessageID = topID
channel.Pts = pts
channel.PinnedMessageID = latestPinned
if err := refreshChannelDialogsAfterDeleteTx(ctx, tx, channel); err != nil {
return nil, domain.ChannelUpdateEvent{}, channel, err
}
// 删除后不再对全员刷新 channel_dialogs 缓存行(性能审计 H4aunread 读时由
// 可见 incoming 消息动态派生deleted 自动出列top 由 channels.top_message_id 提供。
event := domain.ChannelUpdateEvent{
ChannelID: channel.ID,
Type: domain.ChannelUpdateDeleteMessages,

View file

@ -65,28 +65,27 @@ func TestChannelStoreSendMessageFansOutDialogRows(t *testing.T) {
t.Fatalf("send channel message: %v", err)
}
var friendTop, friendReadInbox, friendUnread int
if err := pool.QueryRow(ctx, `
SELECT top_message_id, read_inbox_max_id, unread_count
FROM channel_dialogs
WHERE channel_id = $1 AND user_id = $2`, channelID, friend.ID).Scan(&friendTop, &friendReadInbox, &friendUnread); err != nil {
t.Fatalf("read friend dialog row after send: %v", err)
// H4a发送事务不再对全员 upsert channel_dialogs——dialog 投影读时派生。
// friend 视角top 来自 channels.top_message_idunread 由可见 incoming 消息动态重算。
friendView, err := channels.GetChannelDialogs(ctx, friend.ID, []int64{channelID})
if err != nil {
t.Fatalf("get friend dialogs after send: %v", err)
}
if friendTop != sent.Message.ID || friendReadInbox != 0 || friendUnread != 2 {
t.Fatalf("friend dialog row top=%d read=%d unread=%d, want top %d read 0 unread 2", friendTop, friendReadInbox, friendUnread, sent.Message.ID)
if len(friendView.Dialogs) != 1 || friendView.Dialogs[0].TopMessage != sent.Message.ID ||
friendView.Dialogs[0].ReadInboxMaxID != 0 || friendView.Dialogs[0].UnreadCount != 2 {
t.Fatalf("friend derived dialog = %+v, want top %d read 0 unread 2", friendView.Dialogs, sent.Message.ID)
}
var ownerTop, ownerReadInbox, ownerReadOutbox, ownerUnread int
if err := pool.QueryRow(ctx, `
SELECT top_message_id, read_inbox_max_id, read_outbox_max_id, unread_count
FROM channel_dialogs
WHERE channel_id = $1 AND user_id = $2`, channelID, owner.ID).Scan(&ownerTop, &ownerReadInbox, &ownerReadOutbox, &ownerUnread); err != nil {
t.Fatalf("read owner dialog row after send: %v", err)
}
// 发送者自己的 outbox 回执水位不得随发送推进:同账号另一设备会把它当成
// "对端已读"渲染双勾。推进只能来自 peer 的 readHistory见下方断言
if ownerTop != sent.Message.ID || ownerReadInbox != sent.Message.ID || ownerReadOutbox != 0 || ownerUnread != 0 {
t.Fatalf("owner dialog row top=%d read_in=%d read_out=%d unread=%d, want top/read_in %d, read_out 0 before peer read, unread 0", ownerTop, ownerReadInbox, ownerReadOutbox, ownerUnread, sent.Message.ID)
ownerViewBefore, err := channels.GetChannelDialogs(ctx, owner.ID, []int64{channelID})
if err != nil {
t.Fatalf("get owner dialogs after send: %v", err)
}
if len(ownerViewBefore.Dialogs) != 1 || ownerViewBefore.Dialogs[0].TopMessage != sent.Message.ID ||
ownerViewBefore.Dialogs[0].ReadInboxMaxID != sent.Message.ID ||
ownerViewBefore.Dialogs[0].ReadOutboxMaxID != 0 || ownerViewBefore.Dialogs[0].UnreadCount != 0 {
t.Fatalf("owner derived dialog = %+v, want top/read_in %d, read_out 0 before peer read, unread 0", ownerViewBefore.Dialogs, sent.Message.ID)
}
var ownerMemberReadInbox, ownerMemberReadOutbox int
@ -581,15 +580,15 @@ func TestChannelStoreSendMessageSkipDeliveryAdvancesReadBoundary(t *testing.T) {
t.Fatalf("hidden recipients = %+v, want bot skipped", hidden.Recipients)
}
var botDialogTop, botReadInbox, botUnread int
if err := pool.QueryRow(ctx, `
SELECT top_message_id, read_inbox_max_id, unread_count
FROM channel_dialogs
WHERE channel_id = $1 AND user_id = $2`, channelID, bot.ID).Scan(&botDialogTop, &botReadInbox, &botUnread); err != nil {
t.Fatalf("read bot dialog after hidden: %v", err)
// H4askip 成员的读边界推进只写 channel_members 行unread 派生自动把被隐藏
// 消息排除read boundary 已越过它)。
hiddenView, err := channels.GetChannelDialogs(ctx, bot.ID, []int64{channelID})
if err != nil {
t.Fatalf("get bot dialogs after hidden: %v", err)
}
if botDialogTop != created.Message.ID {
t.Fatalf("bot dialog after hidden top=%d, want unchanged create top %d", botDialogTop, created.Message.ID)
if len(hiddenView.Dialogs) != 1 || hiddenView.Dialogs[0].UnreadCount != 0 ||
hiddenView.Dialogs[0].ReadInboxMaxID != hidden.Message.ID {
t.Fatalf("bot derived dialog after hidden = %+v, want unread 0 read %d", hiddenView.Dialogs, hidden.Message.ID)
}
var memberReadInbox int
if err := pool.QueryRow(ctx, `
@ -615,15 +614,14 @@ WHERE channel_id = $1 AND user_id = $2`, channelID, bot.ID).Scan(&memberReadInbo
if !containsInt64ForPostgresTest(visible.Recipients, bot.ID) {
t.Fatalf("visible recipients = %+v, want bot", visible.Recipients)
}
if err := pool.QueryRow(ctx, `
SELECT top_message_id, read_inbox_max_id, unread_count
FROM channel_dialogs
WHERE channel_id = $1 AND user_id = $2`, channelID, bot.ID).Scan(&botDialogTop, &botReadInbox, &botUnread); err != nil {
t.Fatalf("read bot dialog after visible: %v", err)
visibleView, err := channels.GetChannelDialogs(ctx, bot.ID, []int64{channelID})
if err != nil {
t.Fatalf("get bot dialogs after visible: %v", err)
}
if botDialogTop != visible.Message.ID || botReadInbox != hidden.Message.ID || botUnread != 1 {
t.Fatalf("bot dialog after visible top=%d read=%d unread=%d, want top %d read hidden %d unread visible-only",
botDialogTop, botReadInbox, botUnread, visible.Message.ID, hidden.Message.ID)
if len(visibleView.Dialogs) != 1 || visibleView.Dialogs[0].TopMessage != visible.Message.ID ||
visibleView.Dialogs[0].ReadInboxMaxID != hidden.Message.ID || visibleView.Dialogs[0].UnreadCount != 1 {
t.Fatalf("bot derived dialog after visible = %+v, want top %d read hidden %d unread visible-only",
visibleView.Dialogs, visible.Message.ID, hidden.Message.ID)
}
}

View file

@ -40,22 +40,16 @@ func channelDialogDynamicUnreadExistsSQL(readInboxExpr, topIDExpr string) string
)`, readInboxExpr, topIDExpr)
}
// channelDialogVisibleUnreadCountSQL 恒走读时动态派生(性能审计 H4abroadcast/大群
// 自 P0-1 起即读时派生并经生产验证≤1000 megagroup 也统一到同一路径后,发送事务不再
// 对全员 upsert channel_dialogs`channel_dialogs.unread_count` 缓存列不再被任何读路径
// 消费(真值 = read boundary + 可见 incoming 消息重算)。
func channelDialogVisibleUnreadCountSQL(readInboxExpr, topIDExpr string) string {
dynamicCount := channelDialogDynamicUnreadCountSQL(readInboxExpr, topIDExpr)
// LEAST 钳制缓存分支 d.unread_count小群走缓存列可能因逐条自增长过上界到 P1-v 上界;
// 动态分支已被 LIMIT 子查询钳过,外层 LEAST 对两分支统一封顶,保证下发角标 ≤ 上界。
return fmt.Sprintf(`LEAST(CASE
WHEN c.broadcast OR c.participants_count > %d THEN %s
ELSE COALESCE(d.unread_count, %s)
END, %d)`, domain.MaxSynchronousChannelDialogFanout, dynamicCount, dynamicCount, domain.MaxDialogUnreadCount)
return channelDialogDynamicUnreadCountSQL(readInboxExpr, topIDExpr)
}
func channelDialogHasUnreadSQL(readInboxExpr, topIDExpr string) string {
dynamicUnread := channelDialogDynamicUnreadExistsSQL(readInboxExpr, topIDExpr)
return fmt.Sprintf(`CASE
WHEN c.broadcast OR c.participants_count > %d THEN %s
ELSE COALESCE(d.unread_count > 0, %s)
END`, domain.MaxSynchronousChannelDialogFanout, dynamicUnread, dynamicUnread)
return channelDialogDynamicUnreadExistsSQL(readInboxExpr, topIDExpr)
}
func (s *ChannelStore) SetChannelDialogUnreadMark(ctx context.Context, userID, channelID int64, unread bool) (bool, error) {

View file

@ -79,15 +79,14 @@ func TestChannelStoreReadOutboxDoesNotRegressSenderDialogUnread(t *testing.T) {
t.Fatalf("send member message: %v", err)
}
var storedReadInbox, storedUnread int
if err := pool.QueryRow(ctx, `
SELECT read_inbox_max_id, unread_count
FROM channel_dialogs
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedReadInbox, &storedUnread); err != nil {
t.Fatalf("read member dialog after self send: %v", err)
// H4a发送不再写全员 dialog 行sender 自己的 read_inbox 在发送事务内推进
// channel_members 行,读侧 GREATEST(dialog, member) 派生。
memberView, err := channels.GetChannelDialogs(ctx, member.ID, []int64{channelID})
if err != nil {
t.Fatalf("get member dialogs after self send: %v", err)
}
if storedReadInbox != memberMsg.Message.ID || storedUnread != 0 {
t.Fatalf("member dialog after self send read=%d unread=%d, want read %d unread 0", storedReadInbox, storedUnread, memberMsg.Message.ID)
if len(memberView.Dialogs) != 1 || memberView.Dialogs[0].ReadInboxMaxID != memberMsg.Message.ID || memberView.Dialogs[0].UnreadCount != 0 {
t.Fatalf("member derived dialog after self send = %+v, want read %d unread 0", memberView.Dialogs, memberMsg.Message.ID)
}
read, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
@ -102,14 +101,12 @@ WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedReadI
if len(read.OutboxUpdates) != 1 || read.OutboxUpdates[0].UserID != member.ID || read.OutboxUpdates[0].MaxID != memberMsg.Message.ID {
t.Fatalf("read outbox updates = %+v, want member max id %d", read.OutboxUpdates, memberMsg.Message.ID)
}
if err := pool.QueryRow(ctx, `
SELECT read_inbox_max_id, unread_count
FROM channel_dialogs
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&storedReadInbox, &storedUnread); err != nil {
t.Fatalf("read member dialog after owner read: %v", err)
memberView, err = channels.GetChannelDialogs(ctx, member.ID, []int64{channelID})
if err != nil {
t.Fatalf("get member dialogs after owner read: %v", err)
}
if storedReadInbox != memberMsg.Message.ID || storedUnread != 0 {
t.Fatalf("member dialog after owner read read=%d unread=%d, want read %d unread 0", storedReadInbox, storedUnread, memberMsg.Message.ID)
if len(memberView.Dialogs) != 1 || memberView.Dialogs[0].ReadInboxMaxID != memberMsg.Message.ID || memberView.Dialogs[0].UnreadCount != 0 {
t.Fatalf("member derived dialog after owner read = %+v, want read %d unread 0", memberView.Dialogs, memberMsg.Message.ID)
}
}
@ -680,7 +677,7 @@ WHERE id = $1`, channelID, domain.MaxSynchronousChannelDialogFanout+1); err != n
}
}
func TestChannelStoreSmallMegagroupDeleteRefreshesDialogUnreadCache(t *testing.T) {
func TestChannelStoreSmallMegagroupDeleteDerivesUnread(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
@ -761,15 +758,13 @@ func TestChannelStoreSmallMegagroupDeleteRefreshesDialogUnreadCache(t *testing.T
t.Fatalf("send third message: %v", err)
}
var cachedTop, cachedUnread int
if err := pool.QueryRow(ctx, `
SELECT top_message_id, unread_count
FROM channel_dialogs
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop, &cachedUnread); err != nil {
t.Fatalf("read cached dialog before delete: %v", err)
// H4adialog unread 读时派生,不再依赖 channel_dialogs 缓存列刷新。
beforeDelete, err := channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("list dialogs before delete: %v", err)
}
if cachedTop != third.Message.ID || cachedUnread != 3 {
t.Fatalf("cached dialog before delete top=%d unread=%d, want top %d unread 3", cachedTop, cachedUnread, third.Message.ID)
if len(beforeDelete.Dialogs) != 1 || beforeDelete.Dialogs[0].TopMessage != third.Message.ID || beforeDelete.Dialogs[0].UnreadCount != 3 {
t.Fatalf("derived dialog before delete = %+v, want top %d unread 3", beforeDelete.Dialogs, third.Message.ID)
}
if _, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{
@ -780,15 +775,6 @@ WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop,
}); err != nil {
t.Fatalf("delete middle unread message: %v", err)
}
if err := pool.QueryRow(ctx, `
SELECT top_message_id, unread_count
FROM channel_dialogs
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop, &cachedUnread); err != nil {
t.Fatalf("read cached dialog after middle delete: %v", err)
}
if cachedTop != third.Message.ID || cachedUnread != 2 {
t.Fatalf("cached dialog after middle delete top=%d unread=%d, want top %d unread 2", cachedTop, cachedUnread, third.Message.ID)
}
list, err := channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("list dialogs after middle delete: %v", err)
@ -805,15 +791,6 @@ WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop,
}); err != nil {
t.Fatalf("delete top unread message: %v", err)
}
if err := pool.QueryRow(ctx, `
SELECT top_message_id, unread_count
FROM channel_dialogs
WHERE channel_id = $1 AND user_id = $2`, channelID, member.ID).Scan(&cachedTop, &cachedUnread); err != nil {
t.Fatalf("read cached dialog after top delete: %v", err)
}
if cachedTop != first.Message.ID || cachedUnread != 1 {
t.Fatalf("cached dialog after top delete top=%d unread=%d, want top %d unread 1", cachedTop, cachedUnread, first.Message.ID)
}
list, err = channels.ListChannelDialogs(ctx, member.ID, domain.DialogFilter{Limit: 10})
if err != nil {
t.Fatalf("list dialogs after top delete: %v", err)