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