From d469aac87c6373788c1f15e47fc8cf415cbf4f93 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Sat, 9 May 2026 16:40:27 +1200 Subject: [PATCH] Chore: Optimize MarkRead and MarkUnread functions to reduce database calls and improve performance --- internal/storage/messages.go | 116 ++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 36 deletions(-) diff --git a/internal/storage/messages.go b/internal/storage/messages.go index ff77c42..0523112 100644 --- a/internal/storage/messages.go +++ b/internal/storage/messages.go @@ -559,30 +559,52 @@ func LatestID(r *http.Request) (string, error) { // MarkRead will mark a message as read func MarkRead(ids []string) error { - for _, id := range ids { - res, err := sqlf.Update(tenant("mailbox")). - Set("Read", 1). - Where("ID = ?", id). - Where("Read = ?", 0). - ExecAndClose(context.Background(), db) + if len(ids) == 0 { + return nil + } - if err != nil { - continue + args := make([]any, len(ids)) + for i, id := range ids { + args[i] = id + } + placeholder := `(?` + strings.Repeat(",?", len(ids)-1) + `)` + + // Find which messages are actually unread (will change state) + toUpdate := []string{} + rows, err := db.Query(fmt.Sprintf(`SELECT ID FROM %s WHERE Read = 0 AND ID IN %s`, tenant("mailbox"), placeholder), args...) // #nosec + if err != nil { + return err + } + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + _ = rows.Close() + return err } + toUpdate = append(toUpdate, id) + } + _ = rows.Close() - affected, err := res.RowsAffected() - if err != nil || affected == 0 { - continue - } + if len(toUpdate) == 0 { + return nil + } + updateArgs := make([]any, len(toUpdate)) + for i, id := range toUpdate { + updateArgs[i] = id + } + updatePlaceholder := `(?` + strings.Repeat(",?", len(toUpdate)-1) + `)` + + if _, err := db.Exec(fmt.Sprintf(`UPDATE %s SET Read = 1 WHERE ID IN %s`, tenant("mailbox"), updatePlaceholder), updateArgs...); err != nil { // #nosec + return err + } + + for _, id := range toUpdate { logger.Log().Debugf("[db] marked message %s as read", id) - - d := struct { + websockets.Broadcast("update", struct { ID string Read bool - }{ID: id, Read: true} - - websockets.Broadcast("update", d) + }{ID: id, Read: true}) } BroadcastMailboxStats() @@ -592,32 +614,54 @@ func MarkRead(ids []string) error { // MarkUnread will mark a message as unread func MarkUnread(ids []string) error { - for _, id := range ids { - res, err := sqlf.Update(tenant("mailbox")). - Set("Read", 0). - Where("ID = ?", id). - Where("Read = ?", 1). - ExecAndClose(context.Background(), db) + if len(ids) == 0 { + return nil + } - if err != nil { - continue + args := make([]any, len(ids)) + for i, id := range ids { + args[i] = id + } + placeholder := `(?` + strings.Repeat(",?", len(ids)-1) + `)` + + // Find which messages are actually read (will change state) + toUpdate := []string{} + rows, err := db.Query(fmt.Sprintf(`SELECT ID FROM %s WHERE Read = 1 AND ID IN %s`, tenant("mailbox"), placeholder), args...) // #nosec + if err != nil { + return err + } + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + _ = rows.Close() + return err } + toUpdate = append(toUpdate, id) + } + _ = rows.Close() - affected, err := res.RowsAffected() - if err != nil || affected == 0 { - continue - } + if len(toUpdate) == 0 { + return nil + } + updateArgs := make([]any, len(toUpdate)) + for i, id := range toUpdate { + updateArgs[i] = id + } + updatePlaceholder := `(?` + strings.Repeat(",?", len(toUpdate)-1) + `)` + + if _, err := db.Exec(fmt.Sprintf(`UPDATE %s SET Read = 0 WHERE ID IN %s`, tenant("mailbox"), updatePlaceholder), updateArgs...); err != nil { // #nosec + return err + } + + dbLastAction = time.Now() + + for _, id := range toUpdate { logger.Log().Debugf("[db] marked message %s as unread", id) - - dbLastAction = time.Now() - - d := struct { + websockets.Broadcast("update", struct { ID string Read bool - }{ID: id, Read: false} - - websockets.Broadcast("update", d) + }{ID: id, Read: false}) } BroadcastMailboxStats()