forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 12 kB view raw
1package db 2 3import ( 4 "database/sql" 5 "fmt" 6 "strings" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/appview/models" 11 "tangled.org/core/orm" 12) 13 14// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. 15// It will ignore missing refLinks. 16func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 17 var ( 18 issueRefs []models.ReferenceLink 19 pullRefs []models.ReferenceLink 20 ) 21 for _, ref := range refLinks { 22 switch ref.Kind { 23 case models.RefKindIssue: 24 issueRefs = append(issueRefs, ref) 25 case models.RefKindPull: 26 pullRefs = append(pullRefs, ref) 27 } 28 } 29 issueUris, err := findIssueReferences(e, issueRefs) 30 if err != nil { 31 return nil, fmt.Errorf("find issue references: %w", err) 32 } 33 pullUris, err := findPullReferences(e, pullRefs) 34 if err != nil { 35 return nil, fmt.Errorf("find pull references: %w", err) 36 } 37 38 return append(issueUris, pullUris...), nil 39} 40 41func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 42 if len(refLinks) == 0 { 43 return nil, nil 44 } 45 vals := make([]string, len(refLinks)) 46 args := make([]any, 0, len(refLinks)*4) 47 for i, ref := range refLinks { 48 vals[i] = "(?, ?, ?, ?)" 49 args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId) 50 } 51 query := fmt.Sprintf( 52 `with input(owner_did, name, issue_id, comment_id) as ( 53 values %s 54 ) 55 select 56 i.did, i.rkey, 57 c.did, c.rkey 58 from input inp 59 join repos r 60 on r.did = inp.owner_did 61 and r.name = inp.name 62 join issues i 63 on i.repo_at = r.at_uri 64 and i.issue_id = inp.issue_id 65 left join issue_comments c 66 on inp.comment_id is not null 67 and c.issue_at = i.at_uri 68 and c.id = inp.comment_id 69 `, 70 strings.Join(vals, ","), 71 ) 72 rows, err := e.Query(query, args...) 73 if err != nil { 74 return nil, err 75 } 76 defer rows.Close() 77 78 var uris []syntax.ATURI 79 80 for rows.Next() { 81 // Scan rows 82 var issueOwner, issueRkey string 83 var commentOwner, commentRkey sql.NullString 84 var uri syntax.ATURI 85 if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil { 86 return nil, err 87 } 88 if commentOwner.Valid && commentRkey.Valid { 89 uri = syntax.ATURI(fmt.Sprintf( 90 "at://%s/%s/%s", 91 commentOwner.String, 92 tangled.RepoIssueCommentNSID, 93 commentRkey.String, 94 )) 95 } else { 96 uri = syntax.ATURI(fmt.Sprintf( 97 "at://%s/%s/%s", 98 issueOwner, 99 tangled.RepoIssueNSID, 100 issueRkey, 101 )) 102 } 103 uris = append(uris, uri) 104 } 105 if err := rows.Err(); err != nil { 106 return nil, fmt.Errorf("iterate rows: %w", err) 107 } 108 109 return uris, nil 110} 111 112func findPullReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 113 if len(refLinks) == 0 { 114 return nil, nil 115 } 116 vals := make([]string, len(refLinks)) 117 args := make([]any, 0, len(refLinks)*4) 118 for i, ref := range refLinks { 119 vals[i] = "(?, ?, ?, ?)" 120 args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId) 121 } 122 query := fmt.Sprintf( 123 `with input(owner_did, name, pull_id, comment_id) as ( 124 values %s 125 ) 126 select 127 p.owner_did, p.rkey, 128 c.comment_at 129 from input inp 130 join repos r 131 on r.did = inp.owner_did 132 and r.name = inp.name 133 join pulls p 134 on p.repo_at = r.at_uri 135 and p.pull_id = inp.pull_id 136 left join pull_comments c 137 on inp.comment_id is not null 138 and c.repo_at = r.at_uri and c.pull_id = p.pull_id 139 and c.id = inp.comment_id 140 `, 141 strings.Join(vals, ","), 142 ) 143 rows, err := e.Query(query, args...) 144 if err != nil { 145 return nil, err 146 } 147 defer rows.Close() 148 149 var uris []syntax.ATURI 150 151 for rows.Next() { 152 // Scan rows 153 var pullOwner, pullRkey string 154 var commentUri sql.NullString 155 var uri syntax.ATURI 156 if err := rows.Scan(&pullOwner, &pullRkey, &commentUri); err != nil { 157 return nil, err 158 } 159 if commentUri.Valid { 160 // no-op 161 uri = syntax.ATURI(commentUri.String) 162 } else { 163 uri = syntax.ATURI(fmt.Sprintf( 164 "at://%s/%s/%s", 165 pullOwner, 166 tangled.RepoPullNSID, 167 pullRkey, 168 )) 169 } 170 uris = append(uris, uri) 171 } 172 return uris, nil 173} 174 175func putReferences(tx *sql.Tx, fromAt syntax.ATURI, references []syntax.ATURI) error { 176 err := deleteReferences(tx, fromAt) 177 if err != nil { 178 return fmt.Errorf("delete old reference_links: %w", err) 179 } 180 if len(references) == 0 { 181 return nil 182 } 183 184 values := make([]string, 0, len(references)) 185 args := make([]any, 0, len(references)*2) 186 for _, ref := range references { 187 values = append(values, "(?, ?)") 188 args = append(args, fromAt, ref) 189 } 190 _, err = tx.Exec( 191 fmt.Sprintf( 192 `insert into reference_links (from_at, to_at) 193 values %s`, 194 strings.Join(values, ","), 195 ), 196 args..., 197 ) 198 if err != nil { 199 return fmt.Errorf("insert new reference_links: %w", err) 200 } 201 return nil 202} 203 204func deleteReferences(tx *sql.Tx, fromAt syntax.ATURI) error { 205 _, err := tx.Exec(`delete from reference_links where from_at = ?`, fromAt) 206 return err 207} 208 209func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) { 210 var ( 211 conditions []string 212 args []any 213 ) 214 for _, filter := range filters { 215 conditions = append(conditions, filter.Condition()) 216 args = append(args, filter.Arg()...) 217 } 218 219 whereClause := "" 220 if conditions != nil { 221 whereClause = " where " + strings.Join(conditions, " and ") 222 } 223 224 rows, err := e.Query( 225 fmt.Sprintf( 226 `select from_at, to_at from reference_links %s`, 227 whereClause, 228 ), 229 args..., 230 ) 231 if err != nil { 232 return nil, fmt.Errorf("query reference_links: %w", err) 233 } 234 defer rows.Close() 235 236 result := make(map[syntax.ATURI][]syntax.ATURI) 237 238 for rows.Next() { 239 var from, to syntax.ATURI 240 if err := rows.Scan(&from, &to); err != nil { 241 return nil, fmt.Errorf("scan row: %w", err) 242 } 243 244 result[from] = append(result[from], to) 245 } 246 if err := rows.Err(); err != nil { 247 return nil, fmt.Errorf("iterate rows: %w", err) 248 } 249 250 return result, nil 251} 252 253func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) { 254 rows, err := e.Query( 255 `select from_at from reference_links 256 where to_at = ?`, 257 target, 258 ) 259 if err != nil { 260 return nil, fmt.Errorf("query backlinks: %w", err) 261 } 262 defer rows.Close() 263 264 var ( 265 backlinks []models.RichReferenceLink 266 backlinksMap = make(map[string][]syntax.ATURI) 267 ) 268 for rows.Next() { 269 var from syntax.ATURI 270 if err := rows.Scan(&from); err != nil { 271 return nil, fmt.Errorf("scan row: %w", err) 272 } 273 nsid := from.Collection().String() 274 backlinksMap[nsid] = append(backlinksMap[nsid], from) 275 } 276 if err := rows.Err(); err != nil { 277 return nil, fmt.Errorf("iterate rows: %w", err) 278 } 279 280 var ls []models.RichReferenceLink 281 ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID]) 282 if err != nil { 283 return nil, fmt.Errorf("get issue backlinks: %w", err) 284 } 285 backlinks = append(backlinks, ls...) 286 ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID]) 287 if err != nil { 288 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 289 } 290 backlinks = append(backlinks, ls...) 291 ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID]) 292 if err != nil { 293 return nil, fmt.Errorf("get pull backlinks: %w", err) 294 } 295 backlinks = append(backlinks, ls...) 296 ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID]) 297 if err != nil { 298 return nil, fmt.Errorf("get pull_comment backlinks: %w", err) 299 } 300 backlinks = append(backlinks, ls...) 301 302 return backlinks, nil 303} 304 305func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 306 if len(aturis) == 0 { 307 return nil, nil 308 } 309 vals := make([]string, len(aturis)) 310 args := make([]any, 0, len(aturis)*2) 311 for i, aturi := range aturis { 312 vals[i] = "(?, ?)" 313 did := aturi.Authority().String() 314 rkey := aturi.RecordKey().String() 315 args = append(args, did, rkey) 316 } 317 rows, err := e.Query( 318 fmt.Sprintf( 319 `select r.did, r.name, i.issue_id, i.title, i.open 320 from issues i 321 join repos r 322 on r.at_uri = i.repo_at 323 where (i.did, i.rkey) in (%s)`, 324 strings.Join(vals, ","), 325 ), 326 args..., 327 ) 328 if err != nil { 329 return nil, err 330 } 331 defer rows.Close() 332 var refLinks []models.RichReferenceLink 333 for rows.Next() { 334 var l models.RichReferenceLink 335 l.Kind = models.RefKindIssue 336 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 337 return nil, err 338 } 339 refLinks = append(refLinks, l) 340 } 341 if err := rows.Err(); err != nil { 342 return nil, fmt.Errorf("iterate rows: %w", err) 343 } 344 return refLinks, nil 345} 346 347func getIssueCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 348 if len(aturis) == 0 { 349 return nil, nil 350 } 351 filter := orm.FilterIn("c.at_uri", aturis) 352 rows, err := e.Query( 353 fmt.Sprintf( 354 `select r.did, r.name, i.issue_id, c.id, i.title, i.open 355 from issue_comments c 356 join issues i 357 on i.at_uri = c.issue_at 358 join repos r 359 on r.at_uri = i.repo_at 360 where %s`, 361 filter.Condition(), 362 ), 363 filter.Arg()..., 364 ) 365 if err != nil { 366 return nil, err 367 } 368 defer rows.Close() 369 var refLinks []models.RichReferenceLink 370 for rows.Next() { 371 var l models.RichReferenceLink 372 l.Kind = models.RefKindIssue 373 l.CommentId = new(int) 374 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 375 return nil, err 376 } 377 refLinks = append(refLinks, l) 378 } 379 if err := rows.Err(); err != nil { 380 return nil, fmt.Errorf("iterate rows: %w", err) 381 } 382 return refLinks, nil 383} 384 385func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 386 if len(aturis) == 0 { 387 return nil, nil 388 } 389 vals := make([]string, len(aturis)) 390 args := make([]any, 0, len(aturis)*2) 391 for i, aturi := range aturis { 392 vals[i] = "(?, ?)" 393 did := aturi.Authority().String() 394 rkey := aturi.RecordKey().String() 395 args = append(args, did, rkey) 396 } 397 rows, err := e.Query( 398 fmt.Sprintf( 399 `select r.did, r.name, p.pull_id, p.title, p.state 400 from pulls p 401 join repos r 402 on r.at_uri = p.repo_at 403 where (p.owner_did, p.rkey) in (%s)`, 404 strings.Join(vals, ","), 405 ), 406 args..., 407 ) 408 if err != nil { 409 return nil, err 410 } 411 defer rows.Close() 412 var refLinks []models.RichReferenceLink 413 for rows.Next() { 414 var l models.RichReferenceLink 415 l.Kind = models.RefKindPull 416 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil { 417 return nil, err 418 } 419 refLinks = append(refLinks, l) 420 } 421 if err := rows.Err(); err != nil { 422 return nil, fmt.Errorf("iterate rows: %w", err) 423 } 424 return refLinks, nil 425} 426 427func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) { 428 if len(aturis) == 0 { 429 return nil, nil 430 } 431 filter := orm.FilterIn("c.comment_at", aturis) 432 rows, err := e.Query( 433 fmt.Sprintf( 434 `select r.did, r.name, p.pull_id, c.id, p.title, p.state 435 from repos r 436 join pulls p 437 on r.at_uri = p.repo_at 438 join pull_comments c 439 on r.at_uri = c.repo_at and p.pull_id = c.pull_id 440 where %s`, 441 filter.Condition(), 442 ), 443 filter.Arg()..., 444 ) 445 if err != nil { 446 return nil, err 447 } 448 defer rows.Close() 449 var refLinks []models.RichReferenceLink 450 for rows.Next() { 451 var l models.RichReferenceLink 452 l.Kind = models.RefKindPull 453 l.CommentId = new(int) 454 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil { 455 return nil, err 456 } 457 refLinks = append(refLinks, l) 458 } 459 if err := rows.Err(); err != nil { 460 return nil, fmt.Errorf("iterate rows: %w", err) 461 } 462 return refLinks, nil 463}