forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 14 kB view raw
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}