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