this repo has no description
1package db
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "log"
8 "strings"
9 "time"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 "tangled.org/core/appview/models"
13)
14
15func AddStar(e Execer, star *models.Star) error {
16 query := `insert or ignore into stars (starred_by_did, repo_at, rkey) values (?, ?, ?)`
17 _, err := e.Exec(
18 query,
19 star.StarredByDid,
20 star.RepoAt.String(),
21 star.Rkey,
22 )
23 return err
24}
25
26// Get a star record
27func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*models.Star, error) {
28 query := `
29 select starred_by_did, repo_at, created, rkey
30 from stars
31 where starred_by_did = ? and repo_at = ?`
32 row := e.QueryRow(query, starredByDid, repoAt)
33
34 var star models.Star
35 var created string
36 err := row.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey)
37 if err != nil {
38 return nil, err
39 }
40
41 createdAtTime, err := time.Parse(time.RFC3339, created)
42 if err != nil {
43 log.Println("unable to determine followed at time")
44 star.Created = time.Now()
45 } else {
46 star.Created = createdAtTime
47 }
48
49 return &star, nil
50}
51
52// Remove a star
53func DeleteStar(e Execer, starredByDid string, repoAt syntax.ATURI) error {
54 _, err := e.Exec(`delete from stars where starred_by_did = ? and repo_at = ?`, starredByDid, repoAt)
55 return err
56}
57
58// Remove a star
59func DeleteStarByRkey(e Execer, starredByDid string, rkey string) error {
60 _, err := e.Exec(`delete from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey)
61 return err
62}
63
64func GetStarCount(e Execer, repoAt syntax.ATURI) (int, error) {
65 stars := 0
66 err := e.QueryRow(
67 `select count(starred_by_did) from stars where repo_at = ?`, repoAt).Scan(&stars)
68 if err != nil {
69 return 0, err
70 }
71 return stars, nil
72}
73
74// getStarStatuses returns a map of repo URIs to star status for a given user
75// This is an internal helper function to avoid N+1 queries
76func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) {
77 if len(repoAts) == 0 || userDid == "" {
78 return make(map[string]bool), nil
79 }
80
81 placeholders := make([]string, len(repoAts))
82 args := make([]any, len(repoAts)+1)
83 args[0] = userDid
84
85 for i, repoAt := range repoAts {
86 placeholders[i] = "?"
87 args[i+1] = repoAt.String()
88 }
89
90 query := fmt.Sprintf(`
91 SELECT repo_at
92 FROM stars
93 WHERE starred_by_did = ? AND repo_at IN (%s)
94 `, strings.Join(placeholders, ","))
95
96 rows, err := e.Query(query, args...)
97 if err != nil {
98 return nil, err
99 }
100 defer rows.Close()
101
102 result := make(map[string]bool)
103 // Initialize all repos as not starred
104 for _, repoAt := range repoAts {
105 result[repoAt.String()] = false
106 }
107
108 // Mark starred repos as true
109 for rows.Next() {
110 var repoAt string
111 if err := rows.Scan(&repoAt); err != nil {
112 return nil, err
113 }
114 result[repoAt] = true
115 }
116
117 return result, nil
118}
119
120func GetStarStatus(e Execer, userDid string, repoAt syntax.ATURI) bool {
121 statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{repoAt})
122 if err != nil {
123 return false
124 }
125 return statuses[repoAt.String()]
126}
127
128// GetStarStatuses returns a map of repo URIs to star status for a given user
129func GetStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) {
130 return getStarStatuses(e, userDid, repoAts)
131}
132func GetStars(e Execer, limit int, filters ...filter) ([]models.Star, error) {
133 var conditions []string
134 var args []any
135 for _, filter := range filters {
136 conditions = append(conditions, filter.Condition())
137 args = append(args, filter.Arg()...)
138 }
139
140 whereClause := ""
141 if conditions != nil {
142 whereClause = " where " + strings.Join(conditions, " and ")
143 }
144
145 limitClause := ""
146 if limit != 0 {
147 limitClause = fmt.Sprintf(" limit %d", limit)
148 }
149
150 repoQuery := fmt.Sprintf(
151 `select starred_by_did, repo_at, created, rkey
152 from stars
153 %s
154 order by created desc
155 %s`,
156 whereClause,
157 limitClause,
158 )
159 rows, err := e.Query(repoQuery, args...)
160 if err != nil {
161 return nil, err
162 }
163
164 starMap := make(map[string][]models.Star)
165 for rows.Next() {
166 var star models.Star
167 var created string
168 err := rows.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey)
169 if err != nil {
170 return nil, err
171 }
172
173 star.Created = time.Now()
174 if t, err := time.Parse(time.RFC3339, created); err == nil {
175 star.Created = t
176 }
177
178 repoAt := string(star.RepoAt)
179 starMap[repoAt] = append(starMap[repoAt], star)
180 }
181
182 // populate *Repo in each star
183 args = make([]any, len(starMap))
184 i := 0
185 for r := range starMap {
186 args[i] = r
187 i++
188 }
189
190 if len(args) == 0 {
191 return nil, nil
192 }
193
194 repos, err := GetRepos(e, 0, FilterIn("at_uri", args))
195 if err != nil {
196 return nil, err
197 }
198
199 for _, r := range repos {
200 if stars, ok := starMap[string(r.RepoAt())]; ok {
201 for i := range stars {
202 stars[i].Repo = &r
203 }
204 }
205 }
206
207 var stars []models.Star
208 for _, s := range starMap {
209 stars = append(stars, s...)
210 }
211
212 return stars, nil
213}
214
215func CountStars(e Execer, filters ...filter) (int64, error) {
216 var conditions []string
217 var args []any
218 for _, filter := range filters {
219 conditions = append(conditions, filter.Condition())
220 args = append(args, filter.Arg()...)
221 }
222
223 whereClause := ""
224 if conditions != nil {
225 whereClause = " where " + strings.Join(conditions, " and ")
226 }
227
228 repoQuery := fmt.Sprintf(`select count(1) from stars %s`, whereClause)
229 var count int64
230 err := e.QueryRow(repoQuery, args...).Scan(&count)
231
232 if !errors.Is(err, sql.ErrNoRows) && err != nil {
233 return 0, err
234 }
235
236 return count, nil
237}
238
239func GetAllStars(e Execer, limit int) ([]models.Star, error) {
240 var stars []models.Star
241
242 rows, err := e.Query(`
243 select
244 s.starred_by_did,
245 s.repo_at,
246 s.rkey,
247 s.created,
248 r.did,
249 r.name,
250 r.knot,
251 r.rkey,
252 r.created
253 from stars s
254 join repos r on s.repo_at = r.at_uri
255 `)
256
257 if err != nil {
258 return nil, err
259 }
260 defer rows.Close()
261
262 for rows.Next() {
263 var star models.Star
264 var repo models.Repo
265 var starCreatedAt, repoCreatedAt string
266
267 if err := rows.Scan(
268 &star.StarredByDid,
269 &star.RepoAt,
270 &star.Rkey,
271 &starCreatedAt,
272 &repo.Did,
273 &repo.Name,
274 &repo.Knot,
275 &repo.Rkey,
276 &repoCreatedAt,
277 ); err != nil {
278 return nil, err
279 }
280
281 star.Created, err = time.Parse(time.RFC3339, starCreatedAt)
282 if err != nil {
283 star.Created = time.Now()
284 }
285 repo.Created, err = time.Parse(time.RFC3339, repoCreatedAt)
286 if err != nil {
287 repo.Created = time.Now()
288 }
289 star.Repo = &repo
290
291 stars = append(stars, star)
292 }
293
294 if err := rows.Err(); err != nil {
295 return nil, err
296 }
297
298 return stars, nil
299}
300
301// GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week
302func GetTopStarredReposLastWeek(e Execer) ([]models.Repo, error) {
303 // first, get the top repo URIs by star count from the last week
304 query := `
305 with recent_starred_repos as (
306 select distinct repo_at
307 from stars
308 where created >= datetime('now', '-7 days')
309 ),
310 repo_star_counts as (
311 select
312 s.repo_at,
313 count(*) as stars_gained_last_week
314 from stars s
315 join recent_starred_repos rsr on s.repo_at = rsr.repo_at
316 where s.created >= datetime('now', '-7 days')
317 group by s.repo_at
318 )
319 select rsc.repo_at
320 from repo_star_counts rsc
321 order by rsc.stars_gained_last_week desc
322 limit 8
323 `
324
325 rows, err := e.Query(query)
326 if err != nil {
327 return nil, err
328 }
329 defer rows.Close()
330
331 var repoUris []string
332 for rows.Next() {
333 var repoUri string
334 err := rows.Scan(&repoUri)
335 if err != nil {
336 return nil, err
337 }
338 repoUris = append(repoUris, repoUri)
339 }
340
341 if err := rows.Err(); err != nil {
342 return nil, err
343 }
344
345 if len(repoUris) == 0 {
346 return []models.Repo{}, nil
347 }
348
349 // get full repo data
350 repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris))
351 if err != nil {
352 return nil, err
353 }
354
355 // sort repos by the original trending order
356 repoMap := make(map[string]models.Repo)
357 for _, repo := range repos {
358 repoMap[repo.RepoAt().String()] = repo
359 }
360
361 orderedRepos := make([]models.Repo, 0, len(repoUris))
362 for _, uri := range repoUris {
363 if repo, exists := repoMap[uri]; exists {
364 orderedRepos = append(orderedRepos, repo)
365 }
366 }
367
368 return orderedRepos, nil
369}