forked from tangled.org/core
Monorepo for Tangled

appview/db: avoid N+1 queries in follow/star status fetching

Use a bulk fetcher for both.

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

authored by anirudh.fi and committed by Tangled 890e7863 04314f77

Changed files
+158 -13
appview
+65 -5
appview/db/follow.go
··· 229 229 } 230 230 } 231 231 232 + func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]FollowStatus, error) { 233 + if len(subjectDids) == 0 || userDid == "" { 234 + return make(map[string]FollowStatus), nil 235 + } 236 + 237 + result := make(map[string]FollowStatus) 238 + 239 + for _, subjectDid := range subjectDids { 240 + if userDid == subjectDid { 241 + result[subjectDid] = IsSelf 242 + } else { 243 + result[subjectDid] = IsNotFollowing 244 + } 245 + } 246 + 247 + var querySubjects []string 248 + for _, subjectDid := range subjectDids { 249 + if userDid != subjectDid { 250 + querySubjects = append(querySubjects, subjectDid) 251 + } 252 + } 253 + 254 + if len(querySubjects) == 0 { 255 + return result, nil 256 + } 257 + 258 + placeholders := make([]string, len(querySubjects)) 259 + args := make([]any, len(querySubjects)+1) 260 + args[0] = userDid 261 + 262 + for i, subjectDid := range querySubjects { 263 + placeholders[i] = "?" 264 + args[i+1] = subjectDid 265 + } 266 + 267 + query := fmt.Sprintf(` 268 + SELECT subject_did 269 + FROM follows 270 + WHERE user_did = ? AND subject_did IN (%s) 271 + `, strings.Join(placeholders, ",")) 272 + 273 + rows, err := e.Query(query, args...) 274 + if err != nil { 275 + return nil, err 276 + } 277 + defer rows.Close() 278 + 279 + for rows.Next() { 280 + var subjectDid string 281 + if err := rows.Scan(&subjectDid); err != nil { 282 + return nil, err 283 + } 284 + result[subjectDid] = IsFollowing 285 + } 286 + 287 + return result, nil 288 + } 289 + 232 290 func GetFollowStatus(e Execer, userDid, subjectDid string) FollowStatus { 233 - if userDid == subjectDid { 234 - return IsSelf 235 - } else if _, err := GetFollow(e, userDid, subjectDid); err != nil { 291 + statuses, err := getFollowStatuses(e, userDid, []string{subjectDid}) 292 + if err != nil { 236 293 return IsNotFollowing 237 - } else { 238 - return IsFollowing 239 294 } 295 + return statuses[subjectDid] 296 + } 297 + 298 + func GetFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]FollowStatus, error) { 299 + return getFollowStatuses(e, userDid, subjectDids) 240 300 }
+53 -3
appview/db/star.go
··· 94 94 return stars, nil 95 95 } 96 96 97 + // getStarStatuses returns a map of repo URIs to star status for a given user 98 + // This is an internal helper function to avoid N+1 queries 99 + func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { 100 + if len(repoAts) == 0 || userDid == "" { 101 + return make(map[string]bool), nil 102 + } 103 + 104 + placeholders := make([]string, len(repoAts)) 105 + args := make([]any, len(repoAts)+1) 106 + args[0] = userDid 107 + 108 + for i, repoAt := range repoAts { 109 + placeholders[i] = "?" 110 + args[i+1] = repoAt.String() 111 + } 112 + 113 + query := fmt.Sprintf(` 114 + SELECT repo_at 115 + FROM stars 116 + WHERE starred_by_did = ? AND repo_at IN (%s) 117 + `, strings.Join(placeholders, ",")) 118 + 119 + rows, err := e.Query(query, args...) 120 + if err != nil { 121 + return nil, err 122 + } 123 + defer rows.Close() 124 + 125 + result := make(map[string]bool) 126 + // Initialize all repos as not starred 127 + for _, repoAt := range repoAts { 128 + result[repoAt.String()] = false 129 + } 130 + 131 + // Mark starred repos as true 132 + for rows.Next() { 133 + var repoAt string 134 + if err := rows.Scan(&repoAt); err != nil { 135 + return nil, err 136 + } 137 + result[repoAt] = true 138 + } 139 + 140 + return result, nil 141 + } 142 + 97 143 func GetStarStatus(e Execer, userDid string, repoAt syntax.ATURI) bool { 98 - if _, err := GetStar(e, userDid, repoAt); err != nil { 144 + statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{repoAt}) 145 + if err != nil { 99 146 return false 100 - } else { 101 - return true 102 147 } 148 + return statuses[repoAt.String()] 103 149 } 104 150 151 + // GetStarStatuses returns a map of repo URIs to star status for a given user 152 + func GetStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { 153 + return getStarStatuses(e, userDid, repoAts) 154 + } 105 155 func GetStars(e Execer, limit int, filters ...filter) ([]Star, error) { 106 156 var conditions []string 107 157 var args []any
+40 -5
appview/db/timeline.go
··· 3 3 import ( 4 4 "sort" 5 5 "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 6 8 ) 7 9 8 10 type TimelineEvent struct { ··· 30 32 func MakeTimeline(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, error) { 31 33 var events []TimelineEvent 32 34 33 - repos, err := getTimelineRepos(e, limit) 35 + repos, err := getTimelineRepos(e, limit, loggedInUserDid) 34 36 if err != nil { 35 37 return nil, err 36 38 } ··· 61 63 return events, nil 62 64 } 63 65 64 - func getTimelineRepos(e Execer, limit int) ([]TimelineEvent, error) { 66 + func getTimelineRepos(e Execer, limit int, loggedInUserDid string) ([]TimelineEvent, error) { 65 67 repos, err := GetRepos(e, limit) 66 68 if err != nil { 67 69 return nil, err ··· 88 90 uriToRepo[r.RepoAt().String()] = r 89 91 } 90 92 93 + var starStatuses map[string]bool 94 + if loggedInUserDid != "" { 95 + var repoAts []syntax.ATURI 96 + for _, r := range repos { 97 + repoAts = append(repoAts, r.RepoAt()) 98 + } 99 + var err error 100 + starStatuses, err = GetStarStatuses(e, loggedInUserDid, repoAts) 101 + if err != nil { 102 + return nil, err 103 + } 104 + } 105 + 91 106 var events []TimelineEvent 92 107 for _, r := range repos { 93 108 var source *Repo ··· 97 112 } 98 113 } 99 114 115 + var isStarred bool 116 + if starStatuses != nil { 117 + isStarred = starStatuses[r.RepoAt().String()] 118 + } 119 + 120 + var starCount int64 121 + if r.RepoStats != nil { 122 + starCount = int64(r.RepoStats.StarCount) 123 + } 124 + 100 125 events = append(events, TimelineEvent{ 101 - Repo: &r, 102 - EventAt: r.Created, 103 - Source: source, 126 + Repo: &r, 127 + EventAt: r.Created, 128 + Source: source, 129 + IsStarred: isStarred, 130 + StarCount: starCount, 104 131 }) 105 132 } 106 133 ··· 157 184 followStatMap, err := GetFollowerFollowingCounts(e, subjects) 158 185 if err != nil { 159 186 return nil, err 187 + } 188 + 189 + var followStatuses map[string]FollowStatus 190 + if loggedInUserDid != "" { 191 + followStatuses, err = GetFollowStatuses(e, loggedInUserDid, subjects) 192 + if err != nil { 193 + return nil, err 194 + } 160 195 } 161 196 162 197 var events []TimelineEvent