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