This repository has no description
1package db
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "log"
9 "slices"
10 "strings"
11 "time"
12
13 "github.com/bluesky-social/indigo/atproto/syntax"
14 "tangled.org/core/api/tangled"
15 "tangled.org/core/appview/models"
16 "tangled.org/core/appview/pagination"
17 "tangled.org/core/orm"
18)
19
20func RenameRepo(tx *sql.Tx, did, oldRkey, newRkey, newName string) error {
21 newAtURI := fmt.Sprintf("at://%s/sh.tangled.repo/%s", did, newRkey)
22
23 res, err := tx.Exec(
24 `update repos set rkey = ?, name = ?, at_uri = ? where did = ? and rkey = ?`,
25 newRkey, newName, newAtURI, did, oldRkey,
26 )
27 if err != nil {
28 return fmt.Errorf("update repos row: %w", err)
29 }
30 if n, _ := res.RowsAffected(); n == 0 {
31 return fmt.Errorf("no repo row found for did=%s rkey=%s", did, oldRkey)
32 }
33
34 if _, err := tx.Exec(
35 `update pipelines set repo_name = ? where repo_owner = ? and repo_name = ?`,
36 newRkey, did, oldRkey,
37 ); err != nil {
38 return fmt.Errorf("rename pipelines.repo_name: %w", err)
39 }
40
41 return nil
42}
43
44func UpdateRepoDisplayName(e Execer, did, rkey, newName string) error {
45 _, err := e.Exec(
46 `update repos set name = ? where did = ? and rkey = ?`,
47 newName, did, rkey,
48 )
49 return err
50}
51
52func RecordRepoRename(e Execer, ownerDid, oldRkey, repoDid string) error {
53 _, err := e.Exec(
54 `insert into repo_renames (owner_did, old_rkey, repo_did)
55 values (?, ?, ?)
56 on conflict(owner_did, old_rkey) do update set
57 repo_did = excluded.repo_did,
58 renamed_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')`,
59 ownerDid, oldRkey, repoDid,
60 )
61 return err
62}
63
64func DeleteRepoRename(e Execer, ownerDid, oldRkey string) error {
65 _, err := e.Exec(
66 `delete from repo_renames where owner_did = ? and old_rkey = ?`,
67 ownerDid, oldRkey,
68 )
69 return err
70}
71
72func LookupRepoRename(e Execer, ownerDid, oldRkey string) (*models.Repo, error) {
73 var repoDid string
74 err := e.QueryRow(
75 `select repo_did from repo_renames where owner_did = ? and old_rkey = ?`,
76 ownerDid, oldRkey,
77 ).Scan(&repoDid)
78 if err != nil {
79 return nil, err
80 }
81
82 repo, err := GetRepoByDid(e, repoDid)
83 if err != nil {
84 return nil, err
85 }
86 return repo, nil
87}
88
89func GetRepos(e Execer, filters ...orm.Filter) ([]models.Repo, error) {
90 return GetReposPaginated(e, pagination.Page{}, filters...)
91}
92
93func GetReposPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Repo, error) {
94 var conditions []string
95 var args []any
96 for _, filter := range filters {
97 conditions = append(conditions, filter.Condition())
98 args = append(args, filter.Arg()...)
99 }
100
101 whereClause := ""
102 if conditions != nil {
103 whereClause = " where " + strings.Join(conditions, " and ")
104 }
105
106 pageClause := ""
107 if page.Limit != 0 {
108 pageClause = fmt.Sprintf(" limit %d offset %d", page.Limit, page.Offset)
109 }
110
111 // main query to get repos with pagination
112 query := fmt.Sprintf(`
113 select
114 id,
115 did,
116 name,
117 knot,
118 rkey,
119 created,
120 description,
121 website,
122 topics,
123 source,
124 spindle,
125 repo_did
126 from repos
127 %s
128 order by created desc
129 %s
130 `, whereClause, pageClause)
131
132 rows, err := e.Query(query, args...)
133 if err != nil {
134 return nil, err
135 }
136 defer rows.Close()
137
138 repoMap := make(map[string]*models.Repo)
139 for rows.Next() {
140 var repo models.Repo
141 var createdAt string
142 var description, website, topicStr, source, spindle, repoDid sql.NullString
143
144 err := rows.Scan(
145 &repo.Id,
146 &repo.Did,
147 &repo.Name,
148 &repo.Knot,
149 &repo.Rkey,
150 &createdAt,
151 &description,
152 &website,
153 &topicStr,
154 &source,
155 &spindle,
156 &repoDid,
157 )
158 if err != nil {
159 return nil, err
160 }
161
162 // parse created timestamp
163 if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
164 repo.Created = t
165 }
166
167 // handle nullable fields
168 if description.Valid {
169 repo.Description = description.String
170 }
171 if website.Valid {
172 repo.Website = website.String
173 }
174 if topicStr.Valid {
175 repo.Topics = strings.Fields(topicStr.String)
176 }
177 if source.Valid {
178 repo.Source = source.String
179 }
180 if spindle.Valid {
181 repo.Spindle = spindle.String
182 }
183 if repoDid.Valid {
184 repo.RepoDid = repoDid.String
185 }
186
187 repo.RepoStats = &models.RepoStats{}
188 repoMap[repo.RepoDid] = &repo
189 }
190
191 if err = rows.Err(); err != nil {
192 return nil, err
193 }
194
195 // if no repos, return early
196 if len(repoMap) == 0 {
197 return nil, nil
198 }
199
200 // build IN clause for related queries
201 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(repoMap)), ", ")
202 args = make([]any, len(repoMap))
203 i := 0
204 for _, r := range repoMap {
205 args[i] = r.RepoDid
206 i++
207 }
208
209 // get labels for all repos
210 labelsQuery := fmt.Sprintf(
211 `select repo_did, label_at from repo_labels where repo_did in (%s)`,
212 inClause,
213 )
214
215 rows, err = e.Query(labelsQuery, args...)
216 if err != nil {
217 return nil, err
218 }
219 defer rows.Close()
220
221 for rows.Next() {
222 var repoDid, labelat string
223 if err := rows.Scan(&repoDid, &labelat); err != nil {
224 continue
225 }
226 if r, ok := repoMap[repoDid]; ok {
227 r.Labels = append(r.Labels, labelat)
228 }
229 }
230
231 // get primary language for all repos
232 languageQuery := fmt.Sprintf(`
233 select repo_did, language
234 from (
235 select
236 repo_did, language,
237 row_number() over (
238 partition by repo_did
239 order by bytes desc
240 ) as rn
241 from repo_languages
242 where repo_did in (%s)
243 and is_default_ref = 1
244 and language <> ''
245 )
246 where rn = 1
247 `, inClause)
248
249 rows, err = e.Query(languageQuery, args...)
250 if err != nil {
251 return nil, fmt.Errorf("failed to execute lang query: %w", err)
252 }
253 defer rows.Close()
254
255 for rows.Next() {
256 var repoDid, lang string
257 if err := rows.Scan(&repoDid, &lang); err != nil {
258 log.Println("err", "err", err)
259 continue
260 }
261 if r, ok := repoMap[repoDid]; ok {
262 r.RepoStats.Language = lang
263 }
264 }
265 if err = rows.Err(); err != nil {
266 return nil, fmt.Errorf("failed to execute lang query: %w", err)
267 }
268
269 // get star counts
270 starCountQuery := fmt.Sprintf(
271 `select subject, count(1) from stars where subject_type = 'repo' and subject in (%s) group by subject`,
272 inClause,
273 )
274
275 rows, err = e.Query(starCountQuery, args...)
276 if err != nil {
277 return nil, fmt.Errorf("failed to execute star-count query: %w", err)
278 }
279 defer rows.Close()
280
281 for rows.Next() {
282 var repoDid string
283 var count int
284 if err := rows.Scan(&repoDid, &count); err != nil {
285 log.Println("err", "err", err)
286 continue
287 }
288 if r, ok := repoMap[repoDid]; ok {
289 r.RepoStats.StarCount = count
290 }
291 }
292 if err = rows.Err(); err != nil {
293 return nil, fmt.Errorf("failed to execute star-count query: %w", err)
294 }
295
296 // get issue counts
297 issueCountQuery := fmt.Sprintf(`
298 select
299 repo_did,
300 count(case when open = 1 then 1 end) as open_count,
301 count(case when open = 0 then 1 end) as closed_count
302 from issues
303 where repo_did in (%s)
304 group by repo_did
305 `, inClause)
306
307 rows, err = e.Query(issueCountQuery, args...)
308 if err != nil {
309 return nil, fmt.Errorf("failed to execute issue-count query: %w", err)
310 }
311 defer rows.Close()
312
313 for rows.Next() {
314 var repoDid string
315 var open, closed int
316 if err := rows.Scan(&repoDid, &open, &closed); err != nil {
317 log.Println("err", "err", err)
318 continue
319 }
320 if r, ok := repoMap[repoDid]; ok {
321 r.RepoStats.IssueCount.Open = open
322 r.RepoStats.IssueCount.Closed = closed
323 }
324 }
325 if err = rows.Err(); err != nil {
326 return nil, fmt.Errorf("failed to execute issue-count query: %w", err)
327 }
328
329 // get pull counts
330 pullCountQuery := fmt.Sprintf(`
331 select
332 repo_did,
333 count(case when state = ? then 1 end) as open_count,
334 count(case when state = ? then 1 end) as merged_count,
335 count(case when state = ? then 1 end) as closed_count,
336 count(case when state = ? then 1 end) as deleted_count
337 from pulls
338 where repo_did in (%s)
339 group by repo_did
340 `, inClause)
341
342 pullArgs := append([]any{
343 models.PullOpen,
344 models.PullMerged,
345 models.PullClosed,
346 models.PullAbandoned,
347 }, args...)
348
349 rows, err = e.Query(pullCountQuery, pullArgs...)
350 if err != nil {
351 return nil, fmt.Errorf("failed to execute pulls-count query: %w", err)
352 }
353 defer rows.Close()
354
355 for rows.Next() {
356 var repoDid string
357 var open, merged, closed, deleted int
358 if err := rows.Scan(&repoDid, &open, &merged, &closed, &deleted); err != nil {
359 log.Println("err", "err", err)
360 continue
361 }
362 if r, ok := repoMap[repoDid]; ok {
363 r.RepoStats.PullCount.Open = open
364 r.RepoStats.PullCount.Merged = merged
365 r.RepoStats.PullCount.Closed = closed
366 r.RepoStats.PullCount.Deleted = deleted
367 }
368 }
369 if err = rows.Err(); err != nil {
370 return nil, fmt.Errorf("failed to execute pulls-count query: %w", err)
371 }
372
373 // get forks — only query repos with a non-empty repo_did, since source
374 // stores the upstream's repo_did and an empty string would match all
375 var forksArgs []any
376 for _, r := range repoMap {
377 if r.RepoDid != "" {
378 forksArgs = append(forksArgs, r.RepoDid)
379 }
380 }
381
382 if len(forksArgs) > 0 {
383 forksInClause := strings.TrimSuffix(strings.Repeat("?, ", len(forksArgs)), ", ")
384
385 forksCountQuery := fmt.Sprintf(
386 `select source, count(1) from repos where source in (%s) group by source`,
387 forksInClause,
388 )
389
390 rows, err = e.Query(forksCountQuery, forksArgs...)
391 if err != nil {
392 return nil, fmt.Errorf("failed to execute fork-count query: %w", err)
393 }
394 defer rows.Close()
395
396 for rows.Next() {
397 var repodid string
398 var count int
399 if err := rows.Scan(&repodid, &count); err != nil {
400 log.Println("failed to scan fork count", "err", err)
401 continue
402 }
403
404 if r, ok := repoMap[repodid]; ok {
405 r.RepoStats.ForkCount = count
406 }
407 }
408 if err = rows.Err(); err != nil {
409 return nil, fmt.Errorf("failed to execute fork-count query: %w", err)
410 }
411 }
412
413 var repos []models.Repo
414 for _, r := range repoMap {
415 repos = append(repos, *r)
416 }
417
418 // sort by created timestamp (desc)
419 slices.SortFunc(repos, func(a, b models.Repo) int {
420 if a.Created.After(b.Created) {
421 return -1
422 }
423 return 1
424 })
425
426 return repos, nil
427}
428
429// helper to get exactly one repo
430func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) {
431 repos, err := GetReposPaginated(e, pagination.Page{Limit: 1}, filters...)
432 if err != nil {
433 return nil, err
434 }
435
436 if repos == nil {
437 return nil, sql.ErrNoRows
438 }
439
440 if len(repos) != 1 {
441 return nil, fmt.Errorf("too few rows returned")
442 }
443
444 return &repos[0], nil
445}
446
447func CountRepos(e Execer, filters ...orm.Filter) (int64, error) {
448 var conditions []string
449 var args []any
450 for _, filter := range filters {
451 conditions = append(conditions, filter.Condition())
452 args = append(args, filter.Arg()...)
453 }
454
455 whereClause := ""
456 if conditions != nil {
457 whereClause = " where " + strings.Join(conditions, " and ")
458 }
459
460 repoQuery := fmt.Sprintf(`select count(1) from repos %s`, whereClause)
461 var count int64
462 err := e.QueryRow(repoQuery, args...).Scan(&count)
463
464 if !errors.Is(err, sql.ErrNoRows) && err != nil {
465 return 0, err
466 }
467
468 return count, nil
469}
470
471func GetRepoByAtUri(e Execer, atUri string) (*models.Repo, error) {
472 var repo models.Repo
473 var nullableDescription sql.NullString
474 var nullableWebsite sql.NullString
475 var nullableTopicStr sql.NullString
476 var nullableRepoDid sql.NullString
477 var nullableSource sql.NullString
478 var nullableSpindle sql.NullString
479
480 row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics, source, spindle, repo_did from repos where at_uri = ?`, atUri)
481
482 var createdAt string
483 if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &nullableSource, &nullableSpindle, &nullableRepoDid); err != nil {
484 return nil, err
485 }
486 createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
487 repo.Created = createdAtTime
488
489 if nullableDescription.Valid {
490 repo.Description = nullableDescription.String
491 }
492 if nullableWebsite.Valid {
493 repo.Website = nullableWebsite.String
494 }
495 if nullableTopicStr.Valid {
496 repo.Topics = strings.Fields(nullableTopicStr.String)
497 }
498 if nullableSource.Valid {
499 repo.Source = nullableSource.String
500 }
501 if nullableSpindle.Valid {
502 repo.Spindle = nullableSpindle.String
503 }
504 if nullableRepoDid.Valid {
505 repo.RepoDid = nullableRepoDid.String
506 }
507
508 return &repo, nil
509}
510
511func PutRepo(tx *sql.Tx, repo models.Repo) error {
512 var repoDid *string
513 if repo.RepoDid != "" {
514 repoDid = &repo.RepoDid
515 }
516 _, err := tx.Exec(
517 `update repos
518 set name = ?, knot = ?, description = ?, website = ?, topics = ?, repo_did = coalesce(?, repo_did)
519 where did = ? and rkey = ?
520 `,
521 repo.Name, repo.Knot, repo.Description, repo.Website, repo.TopicStr(), repoDid, repo.Did, repo.Rkey,
522 )
523 return err
524}
525
526func AddRepo(tx *sql.Tx, repo *models.Repo) error {
527 var repoDid *string
528 if repo.RepoDid != "" {
529 repoDid = &repo.RepoDid
530 }
531 result, err := tx.Exec(
532 `insert into repos
533 (did, name, knot, rkey, at_uri, description, website, topics, source, repo_did)
534 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
535 repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Website, repo.TopicStr(), repo.Source, repoDid,
536 )
537 if err != nil {
538 return fmt.Errorf("failed to insert repo: %w", err)
539 }
540
541 id, err := result.LastInsertId()
542 if err != nil {
543 return fmt.Errorf("failed to get last insert id: %w", err)
544 }
545 repo.Id = id
546
547 for _, dl := range repo.Labels {
548 if err := SubscribeLabel(tx, &models.RepoLabel{
549 RepoDid: syntax.DID(repo.RepoDid),
550 LabelAt: syntax.ATURI(dl),
551 }); err != nil {
552 return fmt.Errorf("failed to subscribe to label: %w", err)
553 }
554 }
555
556 return nil
557}
558
559func RemoveRepo(e Execer, did, rkey string) error {
560 _, err := e.Exec(`delete from repos where did = ? and rkey = ?`, did, rkey)
561 return err
562}
563
564func GetRepoSource(e Execer, repoDid string) (string, error) {
565 var nullableSource sql.NullString
566 err := e.QueryRow(`select source from repos where repo_did = ?`, repoDid).Scan(&nullableSource)
567 if err != nil {
568 return "", err
569 }
570 return nullableSource.String, nil
571}
572
573func GetRepoSourceRepo(e Execer, repoDid string) (*models.Repo, error) {
574 source, err := GetRepoSource(e, repoDid)
575 if source == "" || errors.Is(err, sql.ErrNoRows) {
576 return nil, nil
577 }
578 if err != nil {
579 return nil, err
580 }
581 if strings.HasPrefix(source, "did:") {
582 return GetRepoByDid(e, source)
583 }
584 return GetRepoByAtUri(e, source)
585}
586
587func GetForksByDid(e Execer, did string) ([]models.Repo, error) {
588 var repos []models.Repo
589
590 rows, err := e.Query(
591 `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.website, r.created, r.source, r.repo_did
592 from repos r
593 left join collaborators c on r.repo_did = c.repo_did
594 where (r.did = ? or c.subject_did = ?)
595 and r.source is not null
596 and r.source != ''
597 order by r.created desc`,
598 did, did,
599 )
600 if err != nil {
601 return nil, err
602 }
603 defer rows.Close()
604
605 for rows.Next() {
606 var repo models.Repo
607 var createdAt string
608 var nullableDescription sql.NullString
609 var nullableWebsite sql.NullString
610 var nullableSource sql.NullString
611 var nullableRepoDid sql.NullString
612
613 err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource, &nullableRepoDid)
614 if err != nil {
615 return nil, err
616 }
617
618 if nullableDescription.Valid {
619 repo.Description = nullableDescription.String
620 }
621 if nullableWebsite.Valid {
622 repo.Website = nullableWebsite.String
623 }
624
625 if nullableSource.Valid {
626 repo.Source = nullableSource.String
627 }
628 if nullableRepoDid.Valid {
629 repo.RepoDid = nullableRepoDid.String
630 }
631
632 createdAtTime, err := time.Parse(time.RFC3339, createdAt)
633 if err != nil {
634 repo.Created = time.Now()
635 } else {
636 repo.Created = createdAtTime
637 }
638
639 repos = append(repos, repo)
640 }
641
642 if err := rows.Err(); err != nil {
643 return nil, err
644 }
645
646 return repos, nil
647}
648
649func GetRepoByDid(e Execer, repoDid string) (*models.Repo, error) {
650 return GetRepo(e, orm.FilterEq("repo_did", repoDid))
651}
652
653func GetForkByRepoDid(e Execer, repoDid string) (*models.Repo, error) {
654 return GetRepo(e, orm.FilterEq("repo_did", repoDid), orm.FilterNotEq("source", ""))
655}
656
657// TODO: just queue every legacy records regardless of target repo has a DID or not.
658// doable after we have `repo_did` column in db for each tables.
659func EnqueuePdsRewritesForRepo(tx *sql.Tx, repoDid, repoAtUri string) error {
660 type record struct {
661 userDidCol string
662 table string
663 nsid syntax.NSID
664 fkCol string
665 fkVal string
666 }
667 sources := []record{
668 {"did", "repos", tangled.RepoNSID, "at_uri", repoAtUri},
669 {"did", "issues", tangled.RepoIssueNSID, "repo_did", repoDid},
670 {"owner_did", "pulls", tangled.RepoPullNSID, "repo_did", repoDid},
671 {"did", "collaborators", tangled.RepoCollaboratorNSID, "repo_did", repoDid},
672 {"did", "artifacts", tangled.RepoArchiveNSID, "repo_did", repoDid},
673 {"did", "stars", tangled.FeedStarNSID, "subject", repoDid},
674 }
675
676 for _, src := range sources {
677 rows, err := tx.Query(
678 fmt.Sprintf(`SELECT %s, rkey FROM %s WHERE %s = ?`, src.userDidCol, src.table, src.fkCol),
679 src.fkVal,
680 )
681 if err != nil {
682 return fmt.Errorf("query %s for pds rewrites: %w", src.table, err)
683 }
684
685 var pairs []struct{ did, rkey string }
686 for rows.Next() {
687 var d, r string
688 if scanErr := rows.Scan(&d, &r); scanErr != nil {
689 rows.Close()
690 return fmt.Errorf("scan %s for pds rewrites: %w", src.table, scanErr)
691 }
692 pairs = append(pairs, struct{ did, rkey string }{d, r})
693 }
694 rows.Close()
695 if rowsErr := rows.Err(); rowsErr != nil {
696 return fmt.Errorf("iterate %s for pds rewrites: %w", src.table, rowsErr)
697 }
698
699 for _, p := range pairs {
700 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(p.did), src.nsid, syntax.RecordKey(p.rkey)); err != nil {
701 return fmt.Errorf("enqueue pds rewrite for %s/%s: %w", src.table, p.rkey, err)
702 }
703 }
704 }
705
706 profileRows, err := tx.Query(
707 `SELECT DISTINCT did FROM profile_pinned_repositories WHERE pin = ?`,
708 repoAtUri,
709 )
710 if err != nil {
711 return fmt.Errorf("query profile_pinned_repositories for pds rewrites: %w", err)
712 }
713 var profileDids []string
714 for profileRows.Next() {
715 var d string
716 if scanErr := profileRows.Scan(&d); scanErr != nil {
717 profileRows.Close()
718 return fmt.Errorf("scan profile_pinned_repositories for pds rewrites: %w", scanErr)
719 }
720 profileDids = append(profileDids, d)
721 }
722 profileRows.Close()
723 if profileRowsErr := profileRows.Err(); profileRowsErr != nil {
724 return fmt.Errorf("iterate profile_pinned_repositories for pds rewrites: %w", profileRowsErr)
725 }
726
727 for _, d := range profileDids {
728 if err := EnqueuePdsRecordMigration(context.Background(), tx, "add-repo-did", syntax.DID(d), tangled.ActorProfileNSID, "self"); err != nil {
729 return fmt.Errorf("enqueue pds rewrite for profile/%s: %w", d, err)
730 }
731 }
732
733 return nil
734}
735
736func CascadeRepoDid(tx *sql.Tx, repoAtUri, repoDid string) error {
737 _, err := tx.Exec(
738 `UPDATE repos SET repo_did = ? WHERE at_uri = ?`,
739 repoDid, repoAtUri,
740 )
741 if err != nil {
742 return fmt.Errorf("cascade repo_did to repos: %w", err)
743 }
744
745 _, err = tx.Exec(
746 `UPDATE repos SET source = ? WHERE source = ?`,
747 repoDid, repoAtUri,
748 )
749 if err != nil {
750 return fmt.Errorf("cascade repo_did to repos.source: %w", err)
751 }
752
753 return nil
754}
755
756func UpdateDescription(e Execer, repoDid, newDescription string) error {
757 _, err := e.Exec(
758 `update repos set description = ? where repo_did = ?`, newDescription, repoDid)
759 return err
760}
761
762func UpdateSpindle(e Execer, repoDid string, spindle *string) error {
763 _, err := e.Exec(
764 `update repos set spindle = ? where repo_did = ?`, spindle, repoDid)
765 return err
766}
767
768func SubscribeLabel(e Execer, rl *models.RepoLabel) error {
769 query := `insert or ignore into repo_labels (repo_did, label_at) values (?, ?)`
770
771 _, err := e.Exec(query, string(rl.RepoDid), rl.LabelAt.String())
772 return err
773}
774
775func UnsubscribeLabel(e Execer, filters ...orm.Filter) error {
776 var conditions []string
777 var args []any
778 for _, filter := range filters {
779 conditions = append(conditions, filter.Condition())
780 args = append(args, filter.Arg()...)
781 }
782
783 whereClause := ""
784 if conditions != nil {
785 whereClause = " where " + strings.Join(conditions, " and ")
786 }
787
788 query := fmt.Sprintf(`delete from repo_labels %s`, whereClause)
789 _, err := e.Exec(query, args...)
790 return err
791}
792
793func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) {
794 var conditions []string
795 var args []any
796 for _, filter := range filters {
797 conditions = append(conditions, filter.Condition())
798 args = append(args, filter.Arg()...)
799 }
800
801 whereClause := ""
802 if conditions != nil {
803 whereClause = " where " + strings.Join(conditions, " and ")
804 }
805
806 query := fmt.Sprintf(`select id, repo_did, label_at from repo_labels %s`, whereClause)
807
808 rows, err := e.Query(query, args...)
809 if err != nil {
810 return nil, err
811 }
812 defer rows.Close()
813
814 var labels []models.RepoLabel
815 for rows.Next() {
816 var label models.RepoLabel
817
818 err := rows.Scan(&label.Id, &label.RepoDid, &label.LabelAt)
819 if err != nil {
820 return nil, err
821 }
822
823 labels = append(labels, label)
824 }
825
826 if err = rows.Err(); err != nil {
827 return nil, err
828 }
829
830 return labels, nil
831}
832
833func GetForkCount(e Execer, sourceDID string) (int, error) {
834 forks := 0
835 err := e.QueryRow(
836 `select count(source) from repos where source = ?`, sourceDID).Scan(&forks)
837 if err != nil {
838 return 0, err
839 }
840 return forks, nil
841}