1package db
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "log"
8 "slices"
9 "strings"
10 "time"
11
12 "github.com/bluesky-social/indigo/atproto/syntax"
13 "tangled.org/core/appview/models"
14)
15
16func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) {
17 repoMap := make(map[syntax.ATURI]*models.Repo)
18
19 var conditions []string
20 var args []any
21 for _, filter := range filters {
22 conditions = append(conditions, filter.Condition())
23 args = append(args, filter.Arg()...)
24 }
25
26 whereClause := ""
27 if conditions != nil {
28 whereClause = " where " + strings.Join(conditions, " and ")
29 }
30
31 limitClause := ""
32 if limit != 0 {
33 limitClause = fmt.Sprintf(" limit %d", limit)
34 }
35
36 repoQuery := fmt.Sprintf(
37 `select
38 did,
39 name,
40 knot,
41 rkey,
42 created,
43 description,
44 source,
45 spindle
46 from
47 repos r
48 %s
49 order by created desc
50 %s`,
51 whereClause,
52 limitClause,
53 )
54 rows, err := e.Query(repoQuery, args...)
55
56 if err != nil {
57 return nil, fmt.Errorf("failed to execute repo query: %w ", err)
58 }
59
60 for rows.Next() {
61 var repo models.Repo
62 var createdAt string
63 var description, source, spindle sql.NullString
64
65 err := rows.Scan(
66 &repo.Did,
67 &repo.Name,
68 &repo.Knot,
69 &repo.Rkey,
70 &createdAt,
71 &description,
72 &source,
73 &spindle,
74 )
75 if err != nil {
76 return nil, fmt.Errorf("failed to execute repo query: %w ", err)
77 }
78
79 if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
80 repo.Created = t
81 }
82 if description.Valid {
83 repo.Description = description.String
84 }
85 if source.Valid {
86 repo.Source = source.String
87 }
88 if spindle.Valid {
89 repo.Spindle = spindle.String
90 }
91
92 repo.RepoStats = &models.RepoStats{}
93 repoMap[repo.RepoAt()] = &repo
94 }
95
96 if err = rows.Err(); err != nil {
97 return nil, fmt.Errorf("failed to execute repo query: %w ", err)
98 }
99
100 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(repoMap)), ", ")
101 args = make([]any, len(repoMap))
102
103 i := 0
104 for _, r := range repoMap {
105 args[i] = r.RepoAt()
106 i++
107 }
108
109 // Get labels for all repos
110 labelsQuery := fmt.Sprintf(
111 `select repo_at, label_at from repo_labels where repo_at in (%s)`,
112 inClause,
113 )
114 rows, err = e.Query(labelsQuery, args...)
115 if err != nil {
116 return nil, fmt.Errorf("failed to execute labels query: %w ", err)
117 }
118 for rows.Next() {
119 var repoat, labelat string
120 if err := rows.Scan(&repoat, &labelat); err != nil {
121 log.Println("err", "err", err)
122 continue
123 }
124 if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
125 r.Labels = append(r.Labels, labelat)
126 }
127 }
128 if err = rows.Err(); err != nil {
129 return nil, fmt.Errorf("failed to execute labels query: %w ", err)
130 }
131
132 languageQuery := fmt.Sprintf(
133 `
134 select repo_at, language
135 from (
136 select
137 repo_at,
138 language,
139 row_number() over (
140 partition by repo_at
141 order by bytes desc
142 ) as rn
143 from repo_languages
144 where repo_at in (%s)
145 and is_default_ref = 1
146 )
147 where rn = 1
148 `,
149 inClause,
150 )
151 rows, err = e.Query(languageQuery, args...)
152 if err != nil {
153 return nil, fmt.Errorf("failed to execute lang query: %w ", err)
154 }
155 for rows.Next() {
156 var repoat, lang string
157 if err := rows.Scan(&repoat, &lang); err != nil {
158 log.Println("err", "err", err)
159 continue
160 }
161 if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
162 r.RepoStats.Language = lang
163 }
164 }
165 if err = rows.Err(); err != nil {
166 return nil, fmt.Errorf("failed to execute lang query: %w ", err)
167 }
168
169 starCountQuery := fmt.Sprintf(
170 `select
171 repo_at, count(1)
172 from stars
173 where repo_at in (%s)
174 group by repo_at`,
175 inClause,
176 )
177 rows, err = e.Query(starCountQuery, args...)
178 if err != nil {
179 return nil, fmt.Errorf("failed to execute star-count query: %w ", err)
180 }
181 for rows.Next() {
182 var repoat string
183 var count int
184 if err := rows.Scan(&repoat, &count); err != nil {
185 log.Println("err", "err", err)
186 continue
187 }
188 if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
189 r.RepoStats.StarCount = count
190 }
191 }
192 if err = rows.Err(); err != nil {
193 return nil, fmt.Errorf("failed to execute star-count query: %w ", err)
194 }
195
196 issueCountQuery := fmt.Sprintf(
197 `select
198 repo_at,
199 count(case when open = 1 then 1 end) as open_count,
200 count(case when open = 0 then 1 end) as closed_count
201 from issues
202 where repo_at in (%s)
203 group by repo_at`,
204 inClause,
205 )
206 rows, err = e.Query(issueCountQuery, args...)
207 if err != nil {
208 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err)
209 }
210 for rows.Next() {
211 var repoat string
212 var open, closed int
213 if err := rows.Scan(&repoat, &open, &closed); err != nil {
214 log.Println("err", "err", err)
215 continue
216 }
217 if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
218 r.RepoStats.IssueCount.Open = open
219 r.RepoStats.IssueCount.Closed = closed
220 }
221 }
222 if err = rows.Err(); err != nil {
223 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err)
224 }
225
226 pullCountQuery := fmt.Sprintf(
227 `select
228 repo_at,
229 count(case when state = ? then 1 end) as open_count,
230 count(case when state = ? then 1 end) as merged_count,
231 count(case when state = ? then 1 end) as closed_count,
232 count(case when state = ? then 1 end) as deleted_count
233 from pulls
234 where repo_at in (%s)
235 group by repo_at`,
236 inClause,
237 )
238 args = append([]any{
239 models.PullOpen,
240 models.PullMerged,
241 models.PullClosed,
242 models.PullDeleted,
243 }, args...)
244 rows, err = e.Query(
245 pullCountQuery,
246 args...,
247 )
248 if err != nil {
249 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err)
250 }
251 for rows.Next() {
252 var repoat string
253 var open, merged, closed, deleted int
254 if err := rows.Scan(&repoat, &open, &merged, &closed, &deleted); err != nil {
255 log.Println("err", "err", err)
256 continue
257 }
258 if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
259 r.RepoStats.PullCount.Open = open
260 r.RepoStats.PullCount.Merged = merged
261 r.RepoStats.PullCount.Closed = closed
262 r.RepoStats.PullCount.Deleted = deleted
263 }
264 }
265 if err = rows.Err(); err != nil {
266 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err)
267 }
268
269 var repos []models.Repo
270 for _, r := range repoMap {
271 repos = append(repos, *r)
272 }
273
274 slices.SortFunc(repos, func(a, b models.Repo) int {
275 if a.Created.After(b.Created) {
276 return -1
277 }
278 return 1
279 })
280
281 return repos, nil
282}
283
284// helper to get exactly one repo
285func GetRepo(e Execer, filters ...filter) (*models.Repo, error) {
286 repos, err := GetRepos(e, 0, filters...)
287 if err != nil {
288 return nil, err
289 }
290
291 if repos == nil {
292 return nil, sql.ErrNoRows
293 }
294
295 if len(repos) != 1 {
296 return nil, fmt.Errorf("too many rows returned")
297 }
298
299 return &repos[0], nil
300}
301
302func CountRepos(e Execer, filters ...filter) (int64, error) {
303 var conditions []string
304 var args []any
305 for _, filter := range filters {
306 conditions = append(conditions, filter.Condition())
307 args = append(args, filter.Arg()...)
308 }
309
310 whereClause := ""
311 if conditions != nil {
312 whereClause = " where " + strings.Join(conditions, " and ")
313 }
314
315 repoQuery := fmt.Sprintf(`select count(1) from repos %s`, whereClause)
316 var count int64
317 err := e.QueryRow(repoQuery, args...).Scan(&count)
318
319 if !errors.Is(err, sql.ErrNoRows) && err != nil {
320 return 0, err
321 }
322
323 return count, nil
324}
325
326func GetRepoByAtUri(e Execer, atUri string) (*models.Repo, error) {
327 var repo models.Repo
328 var nullableDescription sql.NullString
329
330 row := e.QueryRow(`select did, name, knot, created, rkey, description from repos where at_uri = ?`, atUri)
331
332 var createdAt string
333 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription); err != nil {
334 return nil, err
335 }
336 createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
337 repo.Created = createdAtTime
338
339 if nullableDescription.Valid {
340 repo.Description = nullableDescription.String
341 } else {
342 repo.Description = ""
343 }
344
345 return &repo, nil
346}
347
348func AddRepo(e Execer, repo *models.Repo) error {
349 _, err := e.Exec(
350 `insert into repos
351 (did, name, knot, rkey, at_uri, description, source)
352 values (?, ?, ?, ?, ?, ?, ?)`,
353 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source,
354 )
355 return err
356}
357
358func RemoveRepo(e Execer, did, name string) error {
359 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
360 return err
361}
362
363func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) {
364 var nullableSource sql.NullString
365 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource)
366 if err != nil {
367 return "", err
368 }
369 return nullableSource.String, nil
370}
371
372func GetForksByDid(e Execer, did string) ([]models.Repo, error) {
373 var repos []models.Repo
374
375 rows, err := e.Query(
376 `select distinct r.did, r.name, r.knot, r.rkey, r.description, r.created, r.source
377 from repos r
378 left join collaborators c on r.at_uri = c.repo_at
379 where (r.did = ? or c.subject_did = ?)
380 and r.source is not null
381 and r.source != ''
382 order by r.created desc`,
383 did, did,
384 )
385 if err != nil {
386 return nil, err
387 }
388 defer rows.Close()
389
390 for rows.Next() {
391 var repo models.Repo
392 var createdAt string
393 var nullableDescription sql.NullString
394 var nullableSource sql.NullString
395
396 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource)
397 if err != nil {
398 return nil, err
399 }
400
401 if nullableDescription.Valid {
402 repo.Description = nullableDescription.String
403 }
404
405 if nullableSource.Valid {
406 repo.Source = nullableSource.String
407 }
408
409 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
410 if err != nil {
411 repo.Created = time.Now()
412 } else {
413 repo.Created = createdAtTime
414 }
415
416 repos = append(repos, repo)
417 }
418
419 if err := rows.Err(); err != nil {
420 return nil, err
421 }
422
423 return repos, nil
424}
425
426func GetForkByDid(e Execer, did string, name string) (*models.Repo, error) {
427 var repo models.Repo
428 var createdAt string
429 var nullableDescription sql.NullString
430 var nullableSource sql.NullString
431
432 row := e.QueryRow(
433 `select did, name, knot, rkey, description, created, source
434 from repos
435 where did = ? and name = ? and source is not null and source != ''`,
436 did, name,
437 )
438
439 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource)
440 if err != nil {
441 return nil, err
442 }
443
444 if nullableDescription.Valid {
445 repo.Description = nullableDescription.String
446 }
447
448 if nullableSource.Valid {
449 repo.Source = nullableSource.String
450 }
451
452 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
453 if err != nil {
454 repo.Created = time.Now()
455 } else {
456 repo.Created = createdAtTime
457 }
458
459 return &repo, nil
460}
461
462func UpdateDescription(e Execer, repoAt, newDescription string) error {
463 _, err := e.Exec(
464 `update repos set description = ? where at_uri = ?`, newDescription, repoAt)
465 return err
466}
467
468func UpdateSpindle(e Execer, repoAt string, spindle *string) error {
469 _, err := e.Exec(
470 `update repos set spindle = ? where at_uri = ?`, spindle, repoAt)
471 return err
472}
473
474func SubscribeLabel(e Execer, rl *models.RepoLabel) error {
475 query := `insert or ignore into repo_labels (repo_at, label_at) values (?, ?)`
476
477 _, err := e.Exec(query, rl.RepoAt.String(), rl.LabelAt.String())
478 return err
479}
480
481func UnsubscribeLabel(e Execer, filters ...filter) error {
482 var conditions []string
483 var args []any
484 for _, filter := range filters {
485 conditions = append(conditions, filter.Condition())
486 args = append(args, filter.Arg()...)
487 }
488
489 whereClause := ""
490 if conditions != nil {
491 whereClause = " where " + strings.Join(conditions, " and ")
492 }
493
494 query := fmt.Sprintf(`delete from repo_labels %s`, whereClause)
495 _, err := e.Exec(query, args...)
496 return err
497}
498
499func GetRepoLabels(e Execer, filters ...filter) ([]models.RepoLabel, error) {
500 var conditions []string
501 var args []any
502 for _, filter := range filters {
503 conditions = append(conditions, filter.Condition())
504 args = append(args, filter.Arg()...)
505 }
506
507 whereClause := ""
508 if conditions != nil {
509 whereClause = " where " + strings.Join(conditions, " and ")
510 }
511
512 query := fmt.Sprintf(`select id, repo_at, label_at from repo_labels %s`, whereClause)
513
514 rows, err := e.Query(query, args...)
515 if err != nil {
516 return nil, err
517 }
518 defer rows.Close()
519
520 var labels []models.RepoLabel
521 for rows.Next() {
522 var label models.RepoLabel
523
524 err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt)
525 if err != nil {
526 return nil, err
527 }
528
529 labels = append(labels, label)
530 }
531
532 if err = rows.Err(); err != nil {
533 return nil, err
534 }
535
536 return labels, nil
537}