forked from
tangled.org/core
Monorepo for Tangled
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 = ? and from_at <> 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, target, 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, target, 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, target syntax.ATURI, 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 exclude := orm.FilterNotEq("i.at_uri", target)
353 rows, err := e.Query(
354 fmt.Sprintf(
355 `select r.did, r.name, i.issue_id, c.id, i.title, i.open
356 from issue_comments c
357 join issues i
358 on i.at_uri = c.issue_at
359 join repos r
360 on r.at_uri = i.repo_at
361 where %s and %s`,
362 filter.Condition(),
363 exclude.Condition(),
364 ),
365 append(filter.Arg(), exclude.Arg()...)...,
366 )
367 if err != nil {
368 return nil, err
369 }
370 defer rows.Close()
371 var refLinks []models.RichReferenceLink
372 for rows.Next() {
373 var l models.RichReferenceLink
374 l.Kind = models.RefKindIssue
375 l.CommentId = new(int)
376 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
377 return nil, err
378 }
379 refLinks = append(refLinks, l)
380 }
381 if err := rows.Err(); err != nil {
382 return nil, fmt.Errorf("iterate rows: %w", err)
383 }
384 return refLinks, nil
385}
386
387func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
388 if len(aturis) == 0 {
389 return nil, nil
390 }
391 vals := make([]string, len(aturis))
392 args := make([]any, 0, len(aturis)*2)
393 for i, aturi := range aturis {
394 vals[i] = "(?, ?)"
395 did := aturi.Authority().String()
396 rkey := aturi.RecordKey().String()
397 args = append(args, did, rkey)
398 }
399 rows, err := e.Query(
400 fmt.Sprintf(
401 `select r.did, r.name, p.pull_id, p.title, p.state
402 from pulls p
403 join repos r
404 on r.at_uri = p.repo_at
405 where (p.owner_did, p.rkey) in (%s)`,
406 strings.Join(vals, ","),
407 ),
408 args...,
409 )
410 if err != nil {
411 return nil, err
412 }
413 defer rows.Close()
414 var refLinks []models.RichReferenceLink
415 for rows.Next() {
416 var l models.RichReferenceLink
417 l.Kind = models.RefKindPull
418 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
419 return nil, err
420 }
421 refLinks = append(refLinks, l)
422 }
423 if err := rows.Err(); err != nil {
424 return nil, fmt.Errorf("iterate rows: %w", err)
425 }
426 return refLinks, nil
427}
428
429func getPullCommentBacklinks(e Execer, target syntax.ATURI, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
430 if len(aturis) == 0 {
431 return nil, nil
432 }
433 filter := orm.FilterIn("c.comment_at", aturis)
434 exclude := orm.FilterNotEq("p.at_uri", target)
435 rows, err := e.Query(
436 fmt.Sprintf(
437 `select r.did, r.name, p.pull_id, c.id, p.title, p.state
438 from repos r
439 join pulls p
440 on r.at_uri = p.repo_at
441 join pull_comments c
442 on r.at_uri = c.repo_at and p.pull_id = c.pull_id
443 where %s and %s`,
444 filter.Condition(),
445 exclude.Condition(),
446 ),
447 append(filter.Arg(), exclude.Arg()...)...,
448 )
449 if err != nil {
450 return nil, err
451 }
452 defer rows.Close()
453 var refLinks []models.RichReferenceLink
454 for rows.Next() {
455 var l models.RichReferenceLink
456 l.Kind = models.RefKindPull
457 l.CommentId = new(int)
458 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
459 return nil, err
460 }
461 refLinks = append(refLinks, l)
462 }
463 if err := rows.Err(); err != nil {
464 return nil, fmt.Errorf("iterate rows: %w", err)
465 }
466 return refLinks, nil
467}