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