From 143832552205562f559b72b4fbe0193f2c2209ad Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Wed, 3 Sep 2025 14:59:37 +0300 Subject: [PATCH] appview/db: avoid N+1 queries in follow/star status fetching Change-Id: qrltzqmlrllnwnprnlwrkolxwnllvkuz Use a bulk fetcher for both. Signed-off-by: Anirudh Oppiliappan --- appview/db/follow.go | 70 +++++++++++++++++++++++++++++++++++++++--- appview/db/star.go | 56 +++++++++++++++++++++++++++++++-- appview/db/timeline.go | 45 ++++++++++++++++++++++++--- 3 files changed, 158 insertions(+), 13 deletions(-) diff --git a/appview/db/follow.go b/appview/db/follow.go index 67c1f283..6d4ba04f 100644 --- a/appview/db/follow.go +++ b/appview/db/follow.go @@ -229,12 +229,72 @@ func (s FollowStatus) String() string { } } +func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]FollowStatus, error) { + if len(subjectDids) == 0 || userDid == "" { + return make(map[string]FollowStatus), nil + } + + result := make(map[string]FollowStatus) + + for _, subjectDid := range subjectDids { + if userDid == subjectDid { + result[subjectDid] = IsSelf + } else { + result[subjectDid] = IsNotFollowing + } + } + + var querySubjects []string + for _, subjectDid := range subjectDids { + if userDid != subjectDid { + querySubjects = append(querySubjects, subjectDid) + } + } + + if len(querySubjects) == 0 { + return result, nil + } + + placeholders := make([]string, len(querySubjects)) + args := make([]any, len(querySubjects)+1) + args[0] = userDid + + for i, subjectDid := range querySubjects { + placeholders[i] = "?" + args[i+1] = subjectDid + } + + query := fmt.Sprintf(` + SELECT subject_did + FROM follows + WHERE user_did = ? AND subject_did IN (%s) + `, strings.Join(placeholders, ",")) + + rows, err := e.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var subjectDid string + if err := rows.Scan(&subjectDid); err != nil { + return nil, err + } + result[subjectDid] = IsFollowing + } + + return result, nil +} + func GetFollowStatus(e Execer, userDid, subjectDid string) FollowStatus { - if userDid == subjectDid { - return IsSelf - } else if _, err := GetFollow(e, userDid, subjectDid); err != nil { + statuses, err := getFollowStatuses(e, userDid, []string{subjectDid}) + if err != nil { return IsNotFollowing - } else { - return IsFollowing } + return statuses[subjectDid] +} + +func GetFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]FollowStatus, error) { + return getFollowStatuses(e, userDid, subjectDids) } diff --git a/appview/db/star.go b/appview/db/star.go index ce0934d8..b7b44fc7 100644 --- a/appview/db/star.go +++ b/appview/db/star.go @@ -94,14 +94,64 @@ func GetStarCount(e Execer, repoAt syntax.ATURI) (int, error) { return stars, nil } +// getStarStatuses returns a map of repo URIs to star status for a given user +// This is an internal helper function to avoid N+1 queries +func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { + if len(repoAts) == 0 || userDid == "" { + return make(map[string]bool), nil + } + + placeholders := make([]string, len(repoAts)) + args := make([]any, len(repoAts)+1) + args[0] = userDid + + for i, repoAt := range repoAts { + placeholders[i] = "?" + args[i+1] = repoAt.String() + } + + query := fmt.Sprintf(` + SELECT repo_at + FROM stars + WHERE starred_by_did = ? AND repo_at IN (%s) + `, strings.Join(placeholders, ",")) + + rows, err := e.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + result := make(map[string]bool) + // Initialize all repos as not starred + for _, repoAt := range repoAts { + result[repoAt.String()] = false + } + + // Mark starred repos as true + for rows.Next() { + var repoAt string + if err := rows.Scan(&repoAt); err != nil { + return nil, err + } + result[repoAt] = true + } + + return result, nil +} + func GetStarStatus(e Execer, userDid string, repoAt syntax.ATURI) bool { - if _, err := GetStar(e, userDid, repoAt); err != nil { + statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{repoAt}) + if err != nil { return false - } else { - return true } + return statuses[repoAt.String()] } +// GetStarStatuses returns a map of repo URIs to star status for a given user +func GetStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { + return getStarStatuses(e, userDid, repoAts) +} func GetStars(e Execer, limit int, filters ...filter) ([]Star, error) { var conditions []string var args []any diff --git a/appview/db/timeline.go b/appview/db/timeline.go index d52cc313..79504692 100644 --- a/appview/db/timeline.go +++ b/appview/db/timeline.go @@ -3,6 +3,8 @@ package db import ( "sort" "time" + + "github.com/bluesky-social/indigo/atproto/syntax" ) type TimelineEvent struct { @@ -30,7 +32,7 @@ type TimelineEvent struct { func MakeTimeline(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, error) { var events []TimelineEvent - repos, err := getTimelineRepos(e, limit) + repos, err := getTimelineRepos(e, limit, loggedInUserDid) if err != nil { return nil, err } @@ -61,7 +63,7 @@ func MakeTimeline(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, return events, nil } -func getTimelineRepos(e Execer, limit int) ([]TimelineEvent, error) { +func getTimelineRepos(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, error) { repos, err := GetRepos(e, limit) if err != nil { return nil, err @@ -88,6 +90,19 @@ func getTimelineRepos(e Execer, limit int) ([]TimelineEvent, error) { uriToRepo[r.RepoAt().String()] = r } + var starStatuses map[string]bool + if loggedInUserDid != "" { + var repoAts []syntax.ATURI + for _, r := range repos { + repoAts = append(repoAts, r.RepoAt()) + } + var err error + starStatuses, err = GetStarStatuses(e, loggedInUserDid, repoAts) + if err != nil { + return nil, err + } + } + var events []TimelineEvent for _, r := range repos { var source *Repo @@ -97,10 +112,22 @@ func getTimelineRepos(e Execer, limit int) ([]TimelineEvent, error) { } } + var isStarred bool + if starStatuses != nil { + isStarred = starStatuses[r.RepoAt().String()] + } + + var starCount int64 + if r.RepoStats != nil { + starCount = int64(r.RepoStats.StarCount) + } + events = append(events, TimelineEvent{ - Repo: &r, - EventAt: r.Created, - Source: source, + Repo: &r, + EventAt: r.Created, + Source: source, + IsStarred: isStarred, + StarCount: starCount, }) } @@ -159,6 +186,14 @@ func getTimelineFollows(e Execer, limit int, loggedInUserDid string) ([]Timeline return nil, err } + var followStatuses map[string]FollowStatus + if loggedInUserDid != "" { + followStatuses, err = GetFollowStatuses(e, loggedInUserDid, subjects) + if err != nil { + return nil, err + } + } + var events []TimelineEvent for _, f := range follows { profile, _ := profiles[f.SubjectDid] -- 2.43.0