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