This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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