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