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