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