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 "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
135 repo_at, language
136 from
137 repo_languages r1
138 where
139 repo_at IN (%s)
140 and is_default_ref = 1
141 and id = (
142 select id
143 from repo_languages r2
144 where r2.repo_at = r1.repo_at
145 and r2.is_default_ref = 1
146 order by bytes desc
147 limit 1
148 );
149 `,
150 inClause,
151 )
152 rows, err = e.Query(languageQuery, args...)
153 if err != nil {
154 return nil, fmt.Errorf("failed to execute lang query: %w ", err)
155 }
156 for rows.Next() {
157 var repoat, lang string
158 if err := rows.Scan(&repoat, &lang); err != nil {
159 log.Println("err", "err", err)
160 continue
161 }
162 if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
163 r.RepoStats.Language = lang
164 }
165 }
166 if err = rows.Err(); err != nil {
167 return nil, fmt.Errorf("failed to execute lang query: %w ", err)
168 }
169
170 starCountQuery := fmt.Sprintf(
171 `select
172 repo_at, count(1)
173 from stars
174 where repo_at in (%s)
175 group by repo_at`,
176 inClause,
177 )
178 rows, err = e.Query(starCountQuery, args...)
179 if err != nil {
180 return nil, fmt.Errorf("failed to execute star-count query: %w ", err)
181 }
182 for rows.Next() {
183 var repoat string
184 var count int
185 if err := rows.Scan(&repoat, &count); err != nil {
186 log.Println("err", "err", err)
187 continue
188 }
189 if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
190 r.RepoStats.StarCount = count
191 }
192 }
193 if err = rows.Err(); err != nil {
194 return nil, fmt.Errorf("failed to execute star-count query: %w ", err)
195 }
196
197 issueCountQuery := fmt.Sprintf(
198 `select
199 repo_at,
200 count(case when open = 1 then 1 end) as open_count,
201 count(case when open = 0 then 1 end) as closed_count
202 from issues
203 where repo_at in (%s)
204 group by repo_at`,
205 inClause,
206 )
207 rows, err = e.Query(issueCountQuery, args...)
208 if err != nil {
209 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err)
210 }
211 for rows.Next() {
212 var repoat string
213 var open, closed int
214 if err := rows.Scan(&repoat, &open, &closed); err != nil {
215 log.Println("err", "err", err)
216 continue
217 }
218 if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
219 r.RepoStats.IssueCount.Open = open
220 r.RepoStats.IssueCount.Closed = closed
221 }
222 }
223 if err = rows.Err(); err != nil {
224 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err)
225 }
226
227 pullCountQuery := fmt.Sprintf(
228 `select
229 repo_at,
230 count(case when state = ? then 1 end) as open_count,
231 count(case when state = ? then 1 end) as merged_count,
232 count(case when state = ? then 1 end) as closed_count,
233 count(case when state = ? then 1 end) as deleted_count
234 from pulls
235 where repo_at in (%s)
236 group by repo_at`,
237 inClause,
238 )
239 args = append([]any{
240 models.PullOpen,
241 models.PullMerged,
242 models.PullClosed,
243 models.PullDeleted,
244 }, args...)
245 rows, err = e.Query(
246 pullCountQuery,
247 args...,
248 )
249 if err != nil {
250 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err)
251 }
252 for rows.Next() {
253 var repoat string
254 var open, merged, closed, deleted int
255 if err := rows.Scan(&repoat, &open, &merged, &closed, &deleted); err != nil {
256 log.Println("err", "err", err)
257 continue
258 }
259 if r, ok := repoMap[syntax.ATURI(repoat)]; ok {
260 r.RepoStats.PullCount.Open = open
261 r.RepoStats.PullCount.Merged = merged
262 r.RepoStats.PullCount.Closed = closed
263 r.RepoStats.PullCount.Deleted = deleted
264 }
265 }
266 if err = rows.Err(); err != nil {
267 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err)
268 }
269
270 var repos []models.Repo
271 for _, r := range repoMap {
272 repos = append(repos, *r)
273 }
274
275 slices.SortFunc(repos, func(a, b models.Repo) int {
276 if a.Created.After(b.Created) {
277 return -1
278 }
279 return 1
280 })
281
282 return repos, nil
283}
284
285// helper to get exactly one repo
286func GetRepo(e Execer, filters ...filter) (*models.Repo, error) {
287 repos, err := GetRepos(e, 0, filters...)
288 if err != nil {
289 return nil, err
290 }
291
292 if repos == nil {
293 return nil, sql.ErrNoRows
294 }
295
296 if len(repos) != 1 {
297 return nil, fmt.Errorf("too many rows returned")
298 }
299
300 return &repos[0], nil
301}
302
303func CountRepos(e Execer, filters ...filter) (int64, error) {
304 var conditions []string
305 var args []any
306 for _, filter := range filters {
307 conditions = append(conditions, filter.Condition())
308 args = append(args, filter.Arg()...)
309 }
310
311 whereClause := ""
312 if conditions != nil {
313 whereClause = " where " + strings.Join(conditions, " and ")
314 }
315
316 repoQuery := fmt.Sprintf(`select count(1) from repos %s`, whereClause)
317 var count int64
318 err := e.QueryRow(repoQuery, args...).Scan(&count)
319
320 if !errors.Is(err, sql.ErrNoRows) && err != nil {
321 return 0, err
322 }
323
324 return count, nil
325}
326
327func GetRepoByAtUri(e Execer, atUri string) (*models.Repo, error) {
328 var repo models.Repo
329 var nullableDescription sql.NullString
330
331 row := e.QueryRow(`select did, name, knot, created, rkey, description from repos where at_uri = ?`, atUri)
332
333 var createdAt string
334 if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription); err != nil {
335 return nil, err
336 }
337 createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
338 repo.Created = createdAtTime
339
340 if nullableDescription.Valid {
341 repo.Description = nullableDescription.String
342 } else {
343 repo.Description = ""
344 }
345
346 return &repo, nil
347}
348
349func AddRepo(e Execer, repo *models.Repo) error {
350 _, err := e.Exec(
351 `insert into repos
352 (did, name, knot, rkey, at_uri, description, source)
353 values (?, ?, ?, ?, ?, ?, ?)`,
354 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source,
355 )
356 return err
357}
358
359func RemoveRepo(e Execer, did, name string) error {
360 _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
361 return err
362}
363
364func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) {
365 var nullableSource sql.NullString
366 err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource)
367 if err != nil {
368 return "", err
369 }
370 return nullableSource.String, nil
371}
372
373func GetForksByDid(e Execer, did string) ([]models.Repo, error) {
374 var repos []models.Repo
375
376 rows, err := e.Query(
377 `select distinct r.did, r.name, r.knot, r.rkey, r.description, r.created, r.source
378 from repos r
379 left join collaborators c on r.at_uri = c.repo_at
380 where (r.did = ? or c.subject_did = ?)
381 and r.source is not null
382 and r.source != ''
383 order by r.created desc`,
384 did, did,
385 )
386 if err != nil {
387 return nil, err
388 }
389 defer rows.Close()
390
391 for rows.Next() {
392 var repo models.Repo
393 var createdAt string
394 var nullableDescription sql.NullString
395 var nullableSource sql.NullString
396
397 err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource)
398 if err != nil {
399 return nil, err
400 }
401
402 if nullableDescription.Valid {
403 repo.Description = nullableDescription.String
404 }
405
406 if nullableSource.Valid {
407 repo.Source = nullableSource.String
408 }
409
410 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
411 if err != nil {
412 repo.Created = time.Now()
413 } else {
414 repo.Created = createdAtTime
415 }
416
417 repos = append(repos, repo)
418 }
419
420 if err := rows.Err(); err != nil {
421 return nil, err
422 }
423
424 return repos, nil
425}
426
427func GetForkByDid(e Execer, did string, name string) (*models.Repo, error) {
428 var repo models.Repo
429 var createdAt string
430 var nullableDescription sql.NullString
431 var nullableSource sql.NullString
432
433 row := e.QueryRow(
434 `select did, name, knot, rkey, description, created, source
435 from repos
436 where did = ? and name = ? and source is not null and source != ''`,
437 did, name,
438 )
439
440 err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource)
441 if err != nil {
442 return nil, err
443 }
444
445 if nullableDescription.Valid {
446 repo.Description = nullableDescription.String
447 }
448
449 if nullableSource.Valid {
450 repo.Source = nullableSource.String
451 }
452
453 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
454 if err != nil {
455 repo.Created = time.Now()
456 } else {
457 repo.Created = createdAtTime
458 }
459
460 return &repo, nil
461}
462
463func UpdateDescription(e Execer, repoAt, newDescription string) error {
464 _, err := e.Exec(
465 `update repos set description = ? where at_uri = ?`, newDescription, repoAt)
466 return err
467}
468
469func UpdateSpindle(e Execer, repoAt string, spindle *string) error {
470 _, err := e.Exec(
471 `update repos set spindle = ? where at_uri = ?`, spindle, repoAt)
472 return err
473}
474
475func SubscribeLabel(e Execer, rl *models.RepoLabel) error {
476 query := `insert or ignore into repo_labels (repo_at, label_at) values (?, ?)`
477
478 _, err := e.Exec(query, rl.RepoAt.String(), rl.LabelAt.String())
479 return err
480}
481
482func UnsubscribeLabel(e Execer, filters ...filter) error {
483 var conditions []string
484 var args []any
485 for _, filter := range filters {
486 conditions = append(conditions, filter.Condition())
487 args = append(args, filter.Arg()...)
488 }
489
490 whereClause := ""
491 if conditions != nil {
492 whereClause = " where " + strings.Join(conditions, " and ")
493 }
494
495 query := fmt.Sprintf(`delete from repo_labels %s`, whereClause)
496 _, err := e.Exec(query, args...)
497 return err
498}
499
500func GetRepoLabels(e Execer, filters ...filter) ([]models.RepoLabel, error) {
501 var conditions []string
502 var args []any
503 for _, filter := range filters {
504 conditions = append(conditions, filter.Condition())
505 args = append(args, filter.Arg()...)
506 }
507
508 whereClause := ""
509 if conditions != nil {
510 whereClause = " where " + strings.Join(conditions, " and ")
511 }
512
513 query := fmt.Sprintf(`select id, repo_at, label_at from repo_labels %s`, whereClause)
514
515 rows, err := e.Query(query, args...)
516 if err != nil {
517 return nil, err
518 }
519 defer rows.Close()
520
521 var labels []models.RepoLabel
522 for rows.Next() {
523 var label models.RepoLabel
524
525 err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt)
526 if err != nil {
527 return nil, err
528 }
529
530 labels = append(labels, label)
531 }
532
533 if err = rows.Err(); err != nil {
534 return nil, err
535 }
536
537 return labels, nil
538}