forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
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)
13
14type Star struct {
15 StarredByDid string
16 RepoAt syntax.ATURI
17 Created time.Time
18 Rkey string
19
20 // optionally, populate this when querying for reverse mappings
21 Repo *Repo
22}
23
24func (star *Star) ResolveRepo(e Execer) error {
25 if star.Repo != nil {
26 return nil
27 }
28
29 repo, err := GetRepoByAtUri(e, star.RepoAt.String())
30 if err != nil {
31 return err
32 }
33
34 star.Repo = repo
35 return nil
36}
37
38func AddStar(e Execer, star *Star) error {
39 query := `insert or ignore into stars (starred_by_did, repo_at, rkey) values (?, ?, ?)`
40 _, err := e.Exec(
41 query,
42 star.StarredByDid,
43 star.RepoAt.String(),
44 star.Rkey,
45 )
46 return err
47}
48
49// Get a star record
50func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) {
51 query := `
52 select starred_by_did, repo_at, created, rkey
53 from stars
54 where starred_by_did = ? and repo_at = ?`
55 row := e.QueryRow(query, starredByDid, repoAt)
56
57 var star Star
58 var created string
59 err := row.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey)
60 if err != nil {
61 return nil, err
62 }
63
64 createdAtTime, err := time.Parse(time.RFC3339, created)
65 if err != nil {
66 log.Println("unable to determine followed at time")
67 star.Created = time.Now()
68 } else {
69 star.Created = createdAtTime
70 }
71
72 return &star, nil
73}
74
75// Remove a star
76func DeleteStar(e Execer, starredByDid string, repoAt syntax.ATURI) error {
77 _, err := e.Exec(`delete from stars where starred_by_did = ? and repo_at = ?`, starredByDid, repoAt)
78 return err
79}
80
81// Remove a star
82func DeleteStarByRkey(e Execer, starredByDid string, rkey string) error {
83 _, err := e.Exec(`delete from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey)
84 return err
85}
86
87func GetStarCount(e Execer, repoAt syntax.ATURI) (int, error) {
88 stars := 0
89 err := e.QueryRow(
90 `select count(starred_by_did) from stars where repo_at = ?`, repoAt).Scan(&stars)
91 if err != nil {
92 return 0, err
93 }
94 return stars, nil
95}
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
99func 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
143func GetStarStatus(e Execer, userDid string, repoAt syntax.ATURI) bool {
144 statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{repoAt})
145 if err != nil {
146 return false
147 }
148 return statuses[repoAt.String()]
149}
150
151// GetStarStatuses returns a map of repo URIs to star status for a given user
152func GetStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) {
153 return getStarStatuses(e, userDid, repoAts)
154}
155func GetStars(e Execer, limit int, filters ...filter) ([]Star, error) {
156 var conditions []string
157 var args []any
158 for _, filter := range filters {
159 conditions = append(conditions, filter.Condition())
160 args = append(args, filter.Arg()...)
161 }
162
163 whereClause := ""
164 if conditions != nil {
165 whereClause = " where " + strings.Join(conditions, " and ")
166 }
167
168 limitClause := ""
169 if limit != 0 {
170 limitClause = fmt.Sprintf(" limit %d", limit)
171 }
172
173 repoQuery := fmt.Sprintf(
174 `select starred_by_did, repo_at, created, rkey
175 from stars
176 %s
177 order by created desc
178 %s`,
179 whereClause,
180 limitClause,
181 )
182 rows, err := e.Query(repoQuery, args...)
183 if err != nil {
184 return nil, err
185 }
186
187 starMap := make(map[string][]Star)
188 for rows.Next() {
189 var star Star
190 var created string
191 err := rows.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey)
192 if err != nil {
193 return nil, err
194 }
195
196 star.Created = time.Now()
197 if t, err := time.Parse(time.RFC3339, created); err == nil {
198 star.Created = t
199 }
200
201 repoAt := string(star.RepoAt)
202 starMap[repoAt] = append(starMap[repoAt], star)
203 }
204
205 // populate *Repo in each star
206 args = make([]any, len(starMap))
207 i := 0
208 for r := range starMap {
209 args[i] = r
210 i++
211 }
212
213 if len(args) == 0 {
214 return nil, nil
215 }
216
217 repos, err := GetRepos(e, 0, FilterIn("at_uri", args))
218 if err != nil {
219 return nil, err
220 }
221
222 for _, r := range repos {
223 if stars, ok := starMap[string(r.RepoAt())]; ok {
224 for i := range stars {
225 stars[i].Repo = &r
226 }
227 }
228 }
229
230 var stars []Star
231 for _, s := range starMap {
232 stars = append(stars, s...)
233 }
234
235 return stars, nil
236}
237
238func CountStars(e Execer, filters ...filter) (int64, error) {
239 var conditions []string
240 var args []any
241 for _, filter := range filters {
242 conditions = append(conditions, filter.Condition())
243 args = append(args, filter.Arg()...)
244 }
245
246 whereClause := ""
247 if conditions != nil {
248 whereClause = " where " + strings.Join(conditions, " and ")
249 }
250
251 repoQuery := fmt.Sprintf(`select count(1) from stars %s`, whereClause)
252 var count int64
253 err := e.QueryRow(repoQuery, args...).Scan(&count)
254
255 if !errors.Is(err, sql.ErrNoRows) && err != nil {
256 return 0, err
257 }
258
259 return count, nil
260}
261
262func GetAllStars(e Execer, limit int) ([]Star, error) {
263 var stars []Star
264
265 rows, err := e.Query(`
266 select
267 s.starred_by_did,
268 s.repo_at,
269 s.rkey,
270 s.created,
271 r.did,
272 r.name,
273 r.knot,
274 r.rkey,
275 r.created
276 from stars s
277 join repos r on s.repo_at = r.at_uri
278 `)
279
280 if err != nil {
281 return nil, err
282 }
283 defer rows.Close()
284
285 for rows.Next() {
286 var star Star
287 var repo Repo
288 var starCreatedAt, repoCreatedAt string
289
290 if err := rows.Scan(
291 &star.StarredByDid,
292 &star.RepoAt,
293 &star.Rkey,
294 &starCreatedAt,
295 &repo.Did,
296 &repo.Name,
297 &repo.Knot,
298 &repo.Rkey,
299 &repoCreatedAt,
300 ); err != nil {
301 return nil, err
302 }
303
304 star.Created, err = time.Parse(time.RFC3339, starCreatedAt)
305 if err != nil {
306 star.Created = time.Now()
307 }
308 repo.Created, err = time.Parse(time.RFC3339, repoCreatedAt)
309 if err != nil {
310 repo.Created = time.Now()
311 }
312 star.Repo = &repo
313
314 stars = append(stars, star)
315 }
316
317 if err := rows.Err(); err != nil {
318 return nil, err
319 }
320
321 return stars, nil
322}
323
324// GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week
325func GetTopStarredReposLastWeek(e Execer) ([]Repo, error) {
326 // first, get the top repo URIs by star count from the last week
327 query := `
328 with recent_starred_repos as (
329 select distinct repo_at
330 from stars
331 where created >= datetime('now', '-7 days')
332 ),
333 repo_star_counts as (
334 select
335 s.repo_at,
336 count(*) as stars_gained_last_week
337 from stars s
338 join recent_starred_repos rsr on s.repo_at = rsr.repo_at
339 where s.created >= datetime('now', '-7 days')
340 group by s.repo_at
341 )
342 select rsc.repo_at
343 from repo_star_counts rsc
344 order by rsc.stars_gained_last_week desc
345 limit 8
346 `
347
348 rows, err := e.Query(query)
349 if err != nil {
350 return nil, err
351 }
352 defer rows.Close()
353
354 var repoUris []string
355 for rows.Next() {
356 var repoUri string
357 err := rows.Scan(&repoUri)
358 if err != nil {
359 return nil, err
360 }
361 repoUris = append(repoUris, repoUri)
362 }
363
364 if err := rows.Err(); err != nil {
365 return nil, err
366 }
367
368 if len(repoUris) == 0 {
369 return []Repo{}, nil
370 }
371
372 // get full repo data
373 repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris))
374 if err != nil {
375 return nil, err
376 }
377
378 // sort repos by the original trending order
379 repoMap := make(map[string]Repo)
380 for _, repo := range repos {
381 repoMap[repo.RepoAt().String()] = repo
382 }
383
384 orderedRepos := make([]Repo, 0, len(repoUris))
385 for _, uri := range repoUris {
386 if repo, exists := repoMap[uri]; exists {
387 orderedRepos = append(orderedRepos, repo)
388 }
389 }
390
391 return orderedRepos, nil
392}